國慶期間,我造了臺計算機

yes的練級攻略發表於2020-10-10

每個時代,都不會虧待會學習的人。

大家好,我是 yes。

對於我們程式設計師來說計算機的重要性不言而喻,相信大家對計算機內部也有一定的瞭解。

但是大家有沒想過為什麼一堆邏輯閘組合起來就能運算了?它是如何運作來實現加減法的?

為什麼 cpu 會不停地取指執行?是什麼在驅動著它?

今天我就和大家一起來探索一下底層的奧祕,但是術業有專攻,我們大致的瞭解一下即可,很多細節不清晰也不影響。

不過相信通過這篇文章你會對底層有不一樣的認識,包括運算單元、記憶體、時鐘、地址、溢位、補碼等等。

先打個預防針吧,這篇文章有很多電路圖,你可能感覺這啥啊,和我們開發有關係嗎?

看下去你會懂的,雖說平日裡我們都是 CRUD Boy,但是我們也得時刻保持著好奇心,要有求知慾和探索精神。

正文

這個故事得從「電」開始說起。

生活中電無處不在,而它卻時刻保持著神祕感,為何插上電我們的螢幕就會亮?我們的伺服器就能跑?

電是如何來的?

電起源於電子的運動,我們知道一切物質都是由原子組成的,而原子又是由中子、質子和電子構成。

在某種情況下電子從原子中電離出來,這樣電就產生了。

質子和電子都具有帶電荷的特性,質子帶正電荷、電子帶負電荷。

而異電相吸,同電相斥,當質子數和電子數相等的時候是最穩定的,如果數量不平衡也會往趨於平衡的方向發展

像雷雨天氣,雲層下層積累電子而云層頂層失去電子,而閃電就是大量的電子迅速從一端轉移到另一端產生的結果,為了趨於平衡。

題外話:
細心的朋友可能看到這原子核質子不都合在一起了啊,不是說同電相斥嘛?這是因為有個叫強內力的玩意聚集了它們,釋放核能的原子核裂變就是由強內力導致的。

相信大家都做過電池點亮燈泡的物理實驗。

這其實就是電池發生化學反應,在負極產生多餘的電子,然後通過迴路中的原子類似接力的形式,一個原子得到電子之後會傳遞給相鄰的另一個原子,如此迴圈傳遞電路就形成了,最終通過燈泡到達電池的正極。

改裝下再套上個外殼,手電筒就這樣被造出來了。

而手電筒不僅僅可以用來照明,還能用來通訊。相信大家都看過類似的電影場景,我這手電筒的光閃三下我們們就上!

而說到這樣簡易的通訊就不得不提摩爾斯電碼,相信大家也從各渠道對摩爾斯電碼有一定的瞭解,比如「星際穿越」這部賊好看的電影。

在 19 世紀初期,那時候的遠距離通訊還得利用馬車等工具長時間運輸傳遞,人們一直在摸索即時遠距離通訊的方法。那時的摩爾斯就開始埋頭實驗,最終發明了電報。

電報的思想和上述說的手電筒思想一樣,手電筒通訊的思想是通過開關來控制燈的亮暗,而電報利用的是電磁現象。

將導線纏繞在鐵棒上,然後通電之後鐵棒就變成了磁鐵,斷電了磁性又會消失,然後再搞個發聲器,通過磁性來吸引可動棒敲擊發聲。

通電後可動棒被拉下,敲擊下方就會發出 “滴” 的聲音,斷電則可動棒復位,敲擊上方發出 “嗒” 的聲音。將快速的滴答作為點,慢速的滴答作為劃。

通過導線的長距離連線就能實現遠距離通訊,通過判別點和劃的組合查閱摩爾斯電碼表,轉成最終的資訊。

如果要雙向通訊,就再搞一個反過來部署就好了,這就是電報機了。

不過導線是有電阻的,導線越長電阻越大,所以是有距離限制的,不過這難不倒我們,最簡單的方法就是轉發一下。

在中間距離也建個電報站,然後僱一個人,得到傳送方的電報資訊之後,重新敲一遍傳送給真正的接收方,但是這需要多餘的人力,所以可以如下圖所示,搞個棒子連起來帶動下一個開關的輸出

這其實就是繼電器原理,我們來看看繼電器是如何的設計的。

下方通電產生磁力,吸引上方的金屬桿掛下,然後上方形成迴路因此也通電了,這樣遠距離傳輸的微弱電流就被又一次放大輸出了,所以最終的遠距離電報應該是這樣的。

可以看到繼電器這個發明是真的巧妙。

理解了上面所述的電的生成、電報以及繼電器之後我們再來看看二進位制

基於二進位制的數字系統是最簡單的,只有 0 和 1,不能再進一步簡化了,而簡單就代表著清晰,就像開關要麼開要麼關。

而二進位制的組合又可以代表多種可能,比如第一個 0 表示男,1 表示女 ,第二個 0 表示胖,1 表示瘦。

讓我們再回到之前的電池電燈圖中,這次搞兩個開關。

可以得知兩個開關都閉合電燈才會亮,如果轉化成二進位制表示,0 表示開關斷開,1 表示開關閉合,0 表示燈泡不亮,1 表示燈泡亮,總結成一張表格的話就是:

左開關 右開關 燈泡
0 0 0
0 1 0
1 0 0
1 1 1

這其實就是我們熟知的 AND 操作,如果把電路稍微改一下就是 OR 操作了。

如果把很多開關組合起來就能執行簡單的邏輯任務,但是開關需要手動的去控制。

記得之前提到的繼電器嗎?它也能串聯或者並聯電路,而且可以被其他繼電器聯動控制,不需要一個一個撥動,因此用繼電器來組合更加合適,而繼電器的組合稱之為邏輯閘

簡單點的就像下圖所示,開關閉合燈泡就會亮。

有些人覺得這不是多次一舉嗎,這其實是個緩衝器,可以延遲訊號,也可以放大訊號,而且這個電路比較簡單,實際上有很多組合,比如下圖的這個反向操作,開關閉合的燈反而不會亮。

還有像這樣的串聯組合,只有兩個開關都閉合燈泡才會亮。

當然這裡的輸入不一定得是開關,輸出也不一定得是燈泡,只是為了更加直觀的表現出來,不過這樣畫電路太麻煩了,於是電氣工程師們就搞了個符號來表示這些電路,比如上面的串聯其實就是 AND 操作,是與門。

簡化一下上面的圖就變成下面的樣子。

如果電路圖如下所示,就是並聯,隨便一個開關開了燈泡都會亮,這就是或門。

簡化符號是這樣的:

前面還提到個反向操作的,開關閉上燈泡反而不亮的叫反向器,符號如下圖所示。

我們再來看看這樣的電路。

只有當兩個開關都斷開的情況下燈泡才會亮,任何一個開關閉合燈泡都會熄滅,這個操作和 OR 操作相反,稱之為 NOR 即或非門,簡化後的符號比或門多了個小圓圈,代表反向。

或者這樣,組合著畫也一樣。

然後我們再來看看這種電路,只有兩個開關都閉合才會熄滅,這和與門正好相反,稱之為 NAND 與非門。

簡化符號是這樣的,也是多了個圓圈:

我再總結一下這幾個簡化圖,加深一下印象。

二進位制加法機

有了上面這幾樣東西,我們就可以造個二進位制加法機,不要小看加法,因為可以用加法來實現減法、乘法、除法等操作。

加法我們知道會得到當前的和、進位這兩個資訊,例如二進位制中 1 + 1,當前和是 0 ,進位 1。

進位 0 1
0 0 0
1 0 1

可以看到只有 1 +1 進位 1 ,再仔細看看是不是和 AND 操作很像?只有 1 AND 1 結果才為1 。

AND 0 1
0 0 0
1 0 1

我們再來看看當前和的計算

0 1
0 0 1
1 1 0

大家可以在腦子裡面想象下,如果拿 OR 操作來套用的話右下角結果不對,如果是 NAND 操作的話左上角結果不對,所以得兩個結合一下,電路圖如下。

分別通過或門和與非門之後再做與門,出來的結果就是當前和的結果,這個其實就是 XOR 異或門,簡化表示就是:

所以加法需要通過兩個邏輯閘,分別是異或門來操作當前和,與門來操作進位,結合起來如下圖所示:

這其實就是個半加器,簡化的圖如下所示:

那為什麼叫半加器?因為只能一位一位的加,而前一位的進位參與不到下一位的計算,如果要加入進位那下一位的執行就是 A 的當前位 + B 的當前位 + A 和 B 之前的進位。

因此需要改裝一下,兩個半加器合起來再加一個或門。

假設 A 輸入 1 , B 輸入 1, 進位輸入 1,從最左邊開始第一個半加器 S 輸出 0 , CO 輸出 1,第二個半加器 S輸出 1,CO 輸出 0,最終和輸出 1,進位輸出 1,結果沒毛病可行,這叫全加器,簡化一下圖:

全加器有了,我們們得組合起來,並且需要有輸入和輸出,我們通過開關來輸入數字,由燈泡的亮暗顯示結果。

這就是一個 8 位的計算器,有 9 個燈是因為兩個 8 位相加結果可能是 9 位。

然後從最右邊開始如下圖所示接上全加器,進位接地表示 0 輸入

中間的都如下接法,前一個的進位輸出是下一位的進位輸入。

最後一個就是把進位輸出直接接到第九個燈上就行了。

此時你擺動控制皮膚的開關,就可以通過機器得到相加的結果。簡化的畫法如下圖所示:

現在我們已經造出了八位加法器了,如果要 16 位呢?簡單合一下就好了。

當然真實的計算機原理差不多是這樣的,不過會更復雜,比如不會像我們的加法器,一個一個的進位加,而是會先行進位,而且也不會用繼電器,而是電晶體等等。

減法怎麼弄?

加法器我們搞出來了,那減法怎麼做?減法需要有借位操作。

我們先拿熟悉的十進位制來說。假設你的賬戶上限是499,你的透支額度是500,也就是說你的賬戶金額範圍是 -500~499 這 1000 個數字,要求不能用負號來表示。

可以看到這是個三位數,而最大值就到 499 過,說明 500~999 之間的數沒用,那拿來表示負數不就剛剛好嗎?

所以讓 500 表示 - 500 ,501 表示 -499,以此類推。

500,501.......998,999,000,001......498,499讓5、6、7、8、9開頭的數都代表負數,而且是不是看起來還形成了個環形, 499 + 1 就變成 500 了,然後 999 + 1 變成 1000 ,但是隻能三位數表示,所以溢位了變成 000。

這種處理叫 10 的補數,如果要把三位負數轉為 10 的補數,就是讓 999 減去它再加一,也就是說 10 的補數等於 9 的補數加一。

補數的概念:拿 9 的補數來說,將一個數從一串 9 中減去得到的結果就叫這個數 9 的補數,比如 123 ,它是三位數 ,999-123 = 876 所以 123 的 9 的補數就是 876,如果把結果 + 1那就是 10 的補數了。

就拿 -499 來說,我們要轉化成補數,就是 999 - 499 + 1 等於 501 ,看上面的排列確實用 501 來代表 - 499。

那減去一個數不就是加上一個數的負數嗎?所以通過補數我們就不需要做減法,只需要轉成補數再相加就行了!

現在我們再換成二進位制,二進位制相比於十進位制就更簡單了。

拿八位二進位制數來說,範圍是 00000000~11111111, 對應的十進位制是 0~255,但現在我們想讓它能表示負數,前面十進位制的時候我們將 5、6、7、8、9開頭的正數來表示負數,對應於二進位制我們可以將第一位以1開頭的作為負數。

那此時的範圍就是:

如果你理解了上面的十進位制轉化,這個二進位制肯定是沒問題的,這其實就是算出 2 的補數,而 2 的補數又是 1 的補數 +1。

我們拿 125 來舉個例子,125 二進位制表示是 01111101,求 1 的補數就是 11111111 - 01111101,這個減法在二進位制中不需要,因為這其實就是求反,還記得上文提到的反向器嗎?

取反了之後再加一,就得到 2 的補碼。

所以 -125 就是 10000011。

當然這一切的前提都是數字的位數需要固定,所以計算機中的位數就是固定的,超出了就會溢位,到這裡你應該可以理解計算機中的補碼是怎麼來的,而且理解了為什麼最大值 +1會變成最小值?

所以減法我們只需要改造一下上面的加法器,給個開關表示要這個數是負數,如果是負數則進行一波反向器操作然後再 +1,之後再進行加法操作即可得到最終的結果。

乘法和除法我就不分析了,一樣也能通過加減法來實現。

振盪器(時鐘)、鎖存器(觸發器)和計數器

當然這個和我們所認識的計算機還差很多,現在只能進行一些非常簡陋的加減操作,別急我們先來看看這個電路。

這個電路很有意思,當你閉合開關的時候電路通了,此時由於電磁效應可動棒被吸了下來,電路就斷了,斷了之後磁性消失了可動棒又移了上去,這樣電路又通了,如此往復

這種電路叫振盪器,這是一個很關鍵的東西,記住它。

它的來回振盪其實就是在輸出 0 和 1 的交替序列,畫成圖如下所示:

隨著時間的變化在 0 和 1之間交替變化,因此也稱之為時鐘。

一個變化迴圈所需要的時間稱之為週期,頻率是週期的倒數,如果週期是 0.05 秒,那麼頻率就是 20,每秒 20 個迴圈,用赫茲來作其單位,所以就是 20 Hz。

我們再來看下這個電路。

此時燈泡是不亮的。當上面的開關閉合後,左邊的或非門輸出 0 ,右邊的或非門輸出是 1,因此燈泡亮了。
神奇的地方來了,此時你斷開上面的開關,燈泡依然是亮的,因為左邊的或非門輸出還是 0,而或非門只要有一個輸入是 1,輸出就是 0 。

此時如果閉合下面的開關,燈泡就會熄滅,再斷開下面的開關燈泡仍舊不亮。

可以看到這個電路是有記憶功能的,你看如果你發現此時的燈泡是亮的,你就能推斷上一次閉合的是上面的開關,如果此時燈泡是暗的那麼上次閉合的就是下面的開關!

這種電路叫觸發器,其實上面的開關就等於置位(set),下面的開關等於復位(Reset),所以這也叫 R-S觸發器。

不過更有用的電路應該能記住某個特定時間點的上上一個訊號是 0 是 1

所以還需要搞個保持位,使得保持位關了之後,上下兩個開關隨意撥動都不影響之前保持結果(下面的圖復位和置位位置和我們電路圖是相反了,不過沒影響一樣的)。

其實就是當保持位 0 的時候,復位和置位通過與門的輸出肯定是 0 根本影響不到之前的結果。

但是這樣就有三位輸入了,比較麻煩。從上面的觀察來看有意義的輸入其實是上面開下面關,或者上面關下面開,所以一定是相反的。所以搞個反向器這樣就只有兩個輸入了。

這個叫電平觸發的D型觸發器,D表示 Data,資料的輸入。電平觸發就是當保持位為某一個特定電平時 (例子是 1),觸發器就會儲存資料端的輸入值。

理解了保持位之後,我們需要引入時鐘(標誌為 clk),一個有規律的來回變化的時鐘,當時鍾從 1 切換到 0 的時候上一次操作的內容就被儲存了,所以把保持位的輸入替換成時鐘輸入。

這樣的電路叫做電平觸發的D型鎖存器,它表示電路鎖存住一位資料,並保持到將來使用,它也稱之為 1 位儲存器。

有了 1 位儲存器,那多位儲存器就很簡單了,就是將多個鎖存器合在一起,如下圖是八位鎖存器。

這裡還需要提一下邊沿觸發器,不同於電平觸發器的是邊沿觸發器是在 0 變成 1 的瞬間記錄結果,像電平觸發器是在 1 的時候每個結果都會被覆蓋性的記住,在某些場景下邊沿觸發器的瞬時性更合適。

電路圖如下,由兩級 R-S 觸發器連結而成,其實這種電路看不的很亂覺得很複雜沒事,知道結果就行了。

簡化的畫法如下:

然後我們再來看下這個電路:

將振盪器的輸出作為時鐘的輸入,然後反向 Q 端(上圖中下面的Q代表反向Q,圖少了一橫)的輸入又作為 D 的輸入。

出來的波形圖是這樣的,可以看到 Q 的輸出頻率是時鐘的一半,所以這種電路稱為分頻器。

而分頻器的輸出又可以是下一個分頻器的輸入,我們再來看下這個圖:

出來的波形圖是這樣的:

再填上 0 和 1:

從 Q3 開始每一列從下往上看,是不是 0000、0001、0010.... 這就是計數器,把 8 個整合一下放在黑盒中,就構成了 8 位的計數器。

當然這個計數器是非同步的,後面的得等前面的通知,比較不準確,所以更好的是同步計數器,不過比較複雜,這裡就不介紹了。

簡單組裝一下

至此我們已經有了加法器、振盪器(時鐘)、鎖存器(觸發器)和計數器,接下來我們就開始組裝一下它們。

比如現在我們有一個燈泡,想測試一下八個鎖存器,八個鎖存器的話那麼需要 3 個開關來表示具體選擇哪個鎖存器,2 的 3 次方等於8。

中間的黑盒肯定是拿來選擇的,通過開關來控制通路,比較複雜我覺得稍微看看就行,反正就是電路選擇。

輸入的話也不用直接用八個,所以也搞個三個開關。

內部構造我就不貼了,也和選擇器一樣複雜,這叫譯碼器,最終完整電路圖如下:

而是S0、S1、S2 其實就是地址,通過地址來選擇寫入哪個鎖存器中,並且對應輸出結果,這種配置叫讀/寫儲存器,也稱為隨機訪問儲存器即 RAM

因為它能儲存資訊,所以叫儲存器,因為能根據地址選擇來寫入讀取所以是隨機。

上圖電路簡化圖如下,能儲存 8 個獨立的 1 位資料。

兩個 8*1 RAM 結合一下就能表示儲存 8 個獨立的 2 位資料。

如果是通過下面這樣的組合,則能表示 16*1 RAM,那個 DI 其實就是第四根地址線,所以是 2 的 4 次方。

可以看到 RAM 陣列的儲存容量等於 2 的地址數次方,然後注意下我們圖是簡化了的,裡面其實有很多繼電器的,像邏輯閘都是由繼電器構成的,當斷電之後電磁效應就沒了,所有的觸點都回歸原樣,這就是 RAM 為什麼是易失性儲存介質的原因。

我們們現在已經把記憶體給搞出來了

接下來我們的目標就是把要計算的資料輸入記憶體中,然後讓加法器計算了之後把結果寫回記憶體,並且可以再通過記憶體檢視結果,大致的組裝樣子如下:

然後我們可以將加法器和鎖存器結合起來作為一個累加器,即每次加法的值儲存到鎖存器中並作為下一次累加的值。

有了累加器之後,我們可以將儲存器的值傳到累加器中,稱為 Load 裝載,把下一個值新增到累加器中,稱為 Add,然後將結果儲存在某個位置,稱為 Store。

可以通過控制皮膚先往儲存器裡面寫好要操作的值並且可以通過控制皮膚上的燈來檢視記憶體寫入結果,然後一開始訪問儲存器的地址為 0000,由計數器來驅動地址的前進,然後進行相加,最終將結果儲存回 RAM 陣列中,當然也需要設定停止訊號

把我們前面定義的 Load 等操作碼,轉化為特定的程式碼來控制整體的流程(你就認為這程式碼會指示電路做某種操作,沒必要細想反正就是通過邏輯閘組合產生的)。

這個操作碼僅是個助記符,因為地址是固定的,並且操作碼指令位元組是固定長度(1個位元組),所以我們可以在每條操作後面跟上地址,總的而言每條指令(除停止)需要 3 個位元組。

簡單的看下圖,就是在儲存器地址0000處存入以下“程式碼”。

並且可以搞個 Jump 指令用來跳轉地址,可以通過設定計數器來達成跳轉地址的功能,有了跳轉我們就能做迴圈操作了。某些重複的指令只需要編寫一次,通過條件跳轉來完成迴圈,最終的組裝示意圖如下:

2-1 選擇器是切換計數器的地址輸入或者是計算得出的輸入,通過三個 8 位鎖存器來分別代表程式碼,地址高位和低位,上圖來看可能有點繞,不理解細節也沒有關係,大致的流程還是簡單的。

至此我們其實已經組裝了一臺計算機了,之所以能叫計算機而不是計算器是因為它可以根據你寫入儲存器的指令自動取指執行,並且可以進行條件跳轉和迴圈執行自動停止。

計算機的處理器就是我們上面的累加器,可以稱之為算數邏輯單元,即 ALU。

那個計數器就是我們的程式計數器PC。

儲存器就是記憶體了,輸入就是控制皮膚,輸出就是控制皮膚上的燈。

計算機幾個核心模組就都有了。

至於前面我們定義的操作碼其實就是機器語言,而人類為了好記就會搞一些助記符來標識,發展到後來就是組合語言,而組合語言又太麻煩了,因此又抽象搞了高階語言,比如 C、Java 等等。

最後

這篇文章最終所描述的計算機其實是相當簡陋的,真正的計算機也肯定不會這樣造的,比如不會用繼電器,線路也會用各種匯流排啥的搭建起來各種積體電路等等,ALU 也不會如此簡單,會有各種平行計算等等。

主要是想借此大致的說下計算機基本的執行原理和構成,因為本質上的道理是一樣的。如果要我把很多細節都說出來我也不會,我也就懂一點點點點皮毛,我也不是搞硬體的,啥模電的課我也沒上過,我就會裝裝機的水準。

本文大量藉助了《編碼的奧義》一書的例子,或者說是對此書一些章節的梳理和總結,如果對原文有興趣的可以自己購買書籍,如果覺得囊中羞澀可以後臺回「233」,我來幫你想想辦法。


我是 yes,從一點點到億點點,我們下篇見

相關文章