問題所在:閘沒有過去
前兩篇我們用閘打造出強大的東西:挑選一路輸入的多工器、把位元相加的全加器,以及一整套輸出只取決於當下輸入的組合邏輯方塊。問題就出在「當下」。輸入停住,輸出當然跟著保持;可是一旦輸入改變,舊答案瞬間消失,不留痕跡。組合電路就像一台沒有記憶鍵的計算機:它能計算,卻記不住自己剛剛算出的東西。
但沒有記憶的電腦毫無用處。計數器必須知道上一拍數到哪個數;程式必須記住下一條指令的位址;暫存器必須在加法器咀嚼某個值時把它穩穩握住。我們需要一種電路,它的輸出不只取決於當下的輸入,還取決於自身的歷史——也就是之前發生過什麼。具備這種性質的電路有了新名字:循序邏輯,因為它的行為跟隨時間的順序,而不只是一張快照。
會記憶的把戲:回授
由健忘的閘組成的電路怎麼可能記憶?靠一個大膽的動作:把閘的輸出接回它自己的輸入。這個迴圈叫做回授,它讓訊號追著自己的尾巴跑,最後沉澱到一個穩定的值,再由這個值撐住自己——電路於是成了自己的記憶體。經典範例是用兩個交叉耦合的 NOR 閘做成的 SR 栓鎖:每個閘的輸出餵進另一個閘的輸入,圍成一個小環,可以靜止在兩個穩定狀態之一。
回想第 1 篇:任何閘都能單靠 NAND 做出來(NAND 的完備性)——所以栓鎖說穿了仍只是你早已理解的那些開關,只是接成環而非接成一條線。它的兩個輸出標記為 Q 與其互補(常寫成 Q-bar)。脈衝一下 Set 輸入,Q 鎖在 1;脈衝一下 Reset,Q 鎖在 0;兩者都放開,這個環就只是無限期地保持它最後沉澱的值,不需要時脈、不需要更多輸入。被握住的那個值,就是一個被儲存的位元。
SR latch (cross-coupled NOR), behaviour table S R | Q (next) meaning -----+---------------------- 0 0 | Q (hold) remember the old value <-- this is the memory 1 0 | 1 set 0 1 | 0 reset 1 1 | --- forbidden (Q and Q-bar both 0; race on release) The 'hold' row is the whole point: with no command, the bit stays put.
注意那一列被禁止的情況。把 Set 與 Reset 同時拉高,會把兩個輸出都逼成 0,而當你放開它們時,這個環會隨機地往任一邊沉澱——一場你無法預測的擲硬幣。這是我們初次嚐到一個尷尬的事實:循序電路有著組合電路從未有過的時序規則與非法操作。馴服了栓鎖,你就馴服了整台機器;處理不當,你會得到任何真值表都解釋不了的毛刺。
加上時脈:閘控栓鎖
光禿禿的栓鎖在輸入一抖動就立刻反應——對真實機器而言太心急了,因為訊號得穿過漫長的組合邏輯路徑,在沉澱前有一小段時間是垃圾。我們希望栓鎖在我們說「現在」之前先別管它的資料輸入。於是我們加一條致能線,把它和輸入做 AND:致能為高時栓鎖是透通的(跟隨資料輸入);致能為低時栓鎖是不透通的(凍結,保持最後的值)。這條致能訊號,將成為時脈——一支穩定的節拍器,告訴每個記憶元件它「何時」才被允許去看資料。
這個閘控 D 栓鎖(D 代表 Data 資料)是個真正的進步,卻帶著一個微妙的缺陷。只要時脈維持在高準位,它就一直透通,所以若資料輸入在脈衝中途改變,這個改變會直接穿過去。在一個迴圈裡——例如把自己的輸出繞回當作下一筆輸入的計數器——單一個長時脈脈衝,可能讓值在一拍之內繞著迴圈跑好幾圈。我們要的是每拍剛好更新一次,而不是趁時脈高的時候亂成一團。解法是讓元件不對時脈的「準位」反應,而是對時脈的「邊緣」反應。
正反器:一拍、一位元
把兩個閘控栓鎖串接起來——一個主、一個從——由相反的時脈相位驅動,你就得到邊緣觸發的 D 正反器,幾乎每一顆現代晶片裡的主力記憶元件。時脈為低時,主栓鎖透通,安靜地追蹤資料輸入;從栓鎖凍結,對外展示舊值。在上升邊緣,兩者的角色瞬間互換:主栓鎖啪地關上,捕捉那一剎那 D 的值,而從栓鎖剛好開啟到足以把它複製出去。整體效果美得乾淨俐落——正反器只在時脈邊緣那一刻取樣 D 一次,並在整個週期剩下的時間裡把它穩穩握住。
- 時脈為低:主栓鎖透通,跟著 D 隨意游移;從栓鎖關閉,輸出上一個邊緣存下的值。
- 上升邊緣抵達:那一瞬間主栓鎖關閉,凍結邊緣當下 D 的精確值——這就是「取樣」。
- 邊緣剛過:從栓鎖開啟,把主栓鎖凍結的值複製到輸出 Q,從此整個週期都穩如磐石。
- 因為主、從栓鎖絕不在同一刻同時透通,沒有訊號能從頭穿到尾——每拍剛好更新一次。
從一位元到暫存器與計數器
單一個正反器握住一位元,本身沒太大意思。但把 32 個並排,所有的時脈輸入接到同一條線,你就得到一個 32 位元的暫存器:每個時脈邊緣一次拍下 32 條線的快照,並把整個字組握住一個週期。把許多暫存器綁成一個用編號定址的暫存器檔,你就有了 CPU 那本晶片上的小筆記本——握著某條指令當下正在運算之值的暫存器。你之後會遇到的每一個架構暫存器,包括程式計數器,都不過是一排共用同一條時脈的正反器。
現在把暫存器的輸出穿過一點組合邏輯再接回它自己的輸入,魔法就加倍了。把輸出穿過一個 +1 加法器,你就有了計數器:每一拍它存下「自己的值加一」,於是 Q 隨著時脈一齊踏步 0、1、2、3……——正是推著程式計數器一條指令一條指令往前走的引擎。這個「先算再存」的迴圈——組合邏輯算出下一個值、正反器跨過邊緣把它記住——是循序邏輯的通用骨架,也是下一篇有限狀態機的種子。
那個迴圈裡藏著一個必須誠實面對的小麻煩,它正是通往下一篇的橋。組合邏輯算出的新值,必須在下一個時脈邊緣取樣它「之前」抵達正反器的輸入並停止抖動。正反器要求資料在邊緣前的一小段視窗(它的建立時間)以及邊緣後(它的保持時間)內保持穩定;違反這道視窗,它可能存進垃圾,甚至卡在 0 與 1 之間半懸著。處處遵守這條規則的紀律,叫做同步設計,而其中最長的那條計算路徑——關鍵路徑——最終決定了你能把時脈跑多快。這正是第 4 篇要開始的地方。