七、控制器CU
CPU沒有控制器也能正常完成各條指令,得到相應的運算結果或操作輸入輸出設備,只不過CPU內那麼多部件的控制端口都需要人爲置0或置1才能讓它們協調工作。這也就是早期計算機有那麼多開關需要人工撥動來控制計算機的原因。有了控制器,CPU就可以擺脫人工干預,自動有序得執行程序,速度當然也比人工操作快成千上萬倍。
但是控制器(Control Unit)的設計是相對最複雜的部分。它的設計完成需要考慮以下幾大內容:
(1)數據通道(即Data Path)和子操作
(2)指令集的設計
(3)採用微程序還是硬佈線實現CU
這三大部分內容並不是獨立的,而是相互關聯的,往往一部分內容的微調都會影響另兩部分的設計。下面分別針對以上幾點進行詳細分析。
7.1 數據通道(Data Path)和子操作
數據通道就是CPU內相連的各部件之間的數據傳輸通路。比如在數據總線上連接有ROM的輸出端口和IR寄存器的輸入端口,這就形成了一個數據通道,用ROM->IR表示,有了這個通道,ROM就可以輸出數據進IR寄存器。但沒有通道連接IR寄存器的輸出到A寄存器的輸入,因此這個操作無法在硬件上完成。執行每個數據通道就夠成了一個最基本的硬件子操作,這個子操作不可再被拆分,是CPU的原子操作。
爲了順利完成一個ROM->IR子操作的執行,控制器需要發送不同的控制信號給各部件的控制端口。首先,ROMout端口必須置0,以允許ROM輸出數據到數據總線上,同時IRen端口也需要置0,使得IR寄存器在下一個時鐘週期上升沿把數據總線上的數據存入IR寄存器。ROMout和IRen是這個子操作的核心端口,但僅設置兩個0在這兩個端口上還不夠,因爲數據總線是被很多CPU內的部件所共享的,當ROM佔用數據總線時,我們同時還得讓其它任何共享數據總線的部件不能輸出數據到這個總線上,不然就會發生衝突,子操作就無法完成。因此,除了ROMout和IRen置0外,同時PCLD, Aen, Aout, Ten, Tout, ALUout, DEV1en, DEV2en, DEV0en, MEMen, MEMin, MEMout都需要置1,而ALUM和ALUCn這兩個屬於ALU的控制端口的值可以是任意值,即0或1均可,因爲這個子操作不關心ALU運算。
計算機的每一條指令都是由若干個這樣的子操作構成,Gater8也不例外,但不同的硬件設計,即使相同的功能需要的子操作集合也不一樣。比如,對於指令:
IN T
這條指令將從輸入設備DEV0讀取一個字節到T寄存器,它需要以下3個子操作完成:
(i) ROM->IR
(ii) PC+1->PC
(iii) DEV0->T
(i)完成從ROM中取指令到指令寄存器IR,(ii)完成PC寄存器自加1,(iii)完成從輸入設備DEV0讀取1字節到T寄存器。這3個子操作,每個都能在一個時鐘週期內完成,所以一條IN T指令需要3個時鐘週期完成。
上面(i)和(ii)兩個子操作是取指操作,它們是每一條指令的最前面兩個子操作,(iii)是執行子操作。不同的指令可能有1個或2個執行子操作。比如LDR指令(詳見下文)就有2個執行子操作,因此它是4週期指令。
很重要一點是,每一個子操作必須在一個時鐘週期內完成,同時要求這個子操作對應的數據通路在硬件上必須是存在的。對於不能在同一時鐘週期完成的操作必須拆分爲幾個子操作按順序完成。
7.2 指令集的設計
在已完成的硬件上,我們可以設計相應的指令集。指令集的挑選和設計是需要精心考慮的,特別是將用7400系列芯片搭建出來的Gater8,因爲如果設計過於複雜的指令集,電路就會變得相對複雜,所需7400芯片就會增多,同時設計一些用不上或可以用其它指令表示的一些很少用得上的指令也是一種硬件資源浪費。
由於IR寄存器爲8位,Gater8的指令長度爲8位,於是我打算用高4位表示操作碼(Opcode),低4位表示其它功能或不用。經過慎重考慮最後爲Gater8實現以下11條指令:
(1)OUT: 用於輸出A或T寄存器的值到輸出設備DEV1或DEV2,語法格式爲: OUT DEV1, A;
(2)IN: 從輸入設備DEV0讀取1字節到A或T寄存器,語法:IN A;
(3)LDR: 從指定地址的RAM處讀取1字節內容到A寄存器,語法:LDR 0x123; 0x123爲12位地址,下同。
(4)SUB: 執行減法運算,並將結果存入A或T寄存器,語法:SUB A;
(5)LDI: 從ROM內讀取1字節立即數到A或T寄存器,語法:LDI A, #0xAB; '#'符號表示立即數。
(6)ADD: 執行加法運算,並將結果存入A或T寄存器,語法:ADD A;
(7)JMP: 無條件跳轉,語法:JMP 0x123;
(8)AND: 執行布爾與運算,並將結果存入A或T寄存器,語法:AND A;
(9)STR: 將A寄存器的內容寫入指定地址的RAM中,語法:STR 0x123;
(10)OR: 執行布爾或運算,並將結果存入A或T寄存器,語法:OR A;
(11)JNZ: 當A寄存器值不爲0時跳轉,否則不跳轉,語法:JNZ 0x123;
注:在編寫實際Gater8的彙編程序時,上述指令中出現的地址,比如0x123可以用匯編程序中的符號地址代替。如此便可實現變量的定義、運算,以及循環程序的編寫。
上述每條指令對應的子操作和相應的控制端口的置值情況見表2:
表2. Gater8的詳細控制邏輯(查看高清大圖: 右擊->顯示圖片)。
表2中,所有空白格子內爲省略的數字1,'X'表示可以爲任何值,即0或1均可。
CPU的控制器本質上是一個有限狀態機。在任意狀態下,符合一定條件就會進入下一個不同的狀態,下一個狀態由當前狀態以及給出的條件決定,不一定唯一。每一個狀態可用於表達一個數據通路或子操作,比如上面7.1小節分析的IN T指令,一共由三個子操作構成,每一個子操作的不同控制端口輸出可以對應到一個狀態,假設當前狀態爲表中的S3,那麼就控制器就對17個控制端口分別置S3那行對應的值。
上面提到狀態變化由不同條件引起,所有條件由所有輸入到控制器的信號構成。Gater8的控制器使用以下一共7位輸入信號來確定狀態的變化:
(i) IR寄存器的高6位,其中高4位是指令的操作碼,相對低的2位是條件碼(JNZ, LDR, STR, JMP指令除外);
(ii) Zflag標誌位,即零標誌位;
其中IR寄存器的高4位是操作碼,其值決定了具體的指令,第Zflag標誌位信號只有JNZ指令用到,IR寄存器高6位中的最低位(表中名爲OPC2)只有OUT指令用到,IR寄存器的高6位中倒數第二位(表中名爲OPC)用於決定保存到或讀取到A(OPC=0時)或T(OPC=1時)寄存器。
我們對IN T指令的例子再重新詳細分析一下就是:(i) 最初是S0狀態,對應取指令子操作ROM->IR,(ii) 然後無條件轉換到下一個狀態S1(其實是有條件的,就是發生一個時鐘週期),對應的子操作是PC+1->PC,即PC計數器自加1,(iii) 然後查看以下條件:IR寄存器的高4位操作碼爲0011,對應爲IN指令,同時查看高4位後的那一位(取名OPC位)爲1,於是對應的子操作是DEV0->T,如果OPC位值爲0,對應的子操作爲DEV0->A,這兩個子操作都對應到狀態S3,因爲它們都可以在一個時鐘週期內完成。
Gater8的指令集設計採用了不定長格式,LDI, JNZ, LDR, STR, 和JMP這五條爲2字節指令,其它均爲1字節指令。
對於不同的操作碼,IR寄存器的低4位含意是不同的。一共有兩種情況,下面分別圖示加說明。
第一種情況:對於OUT, IN, SUB, LDI, ADD, AND, OR這七條指令,其機器碼結構如圖3所示:
圖3. OUT, IN, SUB, LDI, ADD, AND, OR七條指令的機器碼結構。
在圖3中,'X'表示不使用,其中OPC2只有OUT指令用到,用於表示設備DEV1(OPC2=0時)或DEV2(OPC2=1時)。這7條指令中,除了LDI外,都是單字節指令,LDI的完整指令格式如圖4所示:
圖4. LDI指令機器碼結構。
在圖4中,第1字節和其它6條指令結構一致(LDI指令不使用OPC2),第二字節爲無符號立即數,等價於C語言中的unsigned char類型的常量。
第二種情況:對於JNZ, LDR, STR, 和JMP這4條指令,由於它們都需要涉及地址操作,因此它們都是2字節指令,其機器碼結構如圖5所示:
圖5. JNZ, LDR, STR, 和JMP這4條指令的機器碼結構。
這4條指令的第1字節高4位爲操作碼,低4位提供12位地址的高4位地址,第2字節提供12位地址的低8位地址。爲了完整提供12位地址,第1字節的低4位都用於表示地址,不能用於其它目的,也就沒有OPC位的存在,因此LDR和STR這兩條指令不能選擇讀取或寫入A還是T寄存器,都是固定操作一個默認的寄存器(默認爲A)。
7.3 Gater8的硬佈線控制器實現
在控制器設計階段最重要的成果就是完成7.2小節中的表2。表2完整提供了每條指令的所需要的子操作、每個子操作對應的各部件控制端口的控制信號,以及狀態轉變的條件。
控制器的實現主有兩種方式:
(1)微程序方式:需要微程序ROM,用各個條件作爲該ROM的地址,對應的輸出就是各部件的控制信號。
(2)硬佈線方式:完全用布爾電路實現整個狀態機。
對於Gater8的控制器而言,一共有7個輸入和17個輸出。如果我去掉一個輸出設備,就可以使所需控制的端口數變爲16個,這樣我就可以用2片8位地址輸入8位數據輸出的ROM芯片,7個控制器輸入位做爲地址(最高地址位不用,置0)存儲每個狀態對應的16個控制信號,就完成了微程序式的控制器實現。Nibbler自制CPU就是這麼做的。
但我本人更偏向用硬佈線實現,也是Gater8的實現方式。因爲我一共用了4位操作碼,共可產生16個狀態,每個狀態按上面表2中輸出相應的各控制信號。狀態轉變可以通過計數器(Counter)實現,對4位操作碼對應16個狀態中的哪一個可以通過譯碼器(Decoder)實現,而剩下的每個狀態輸出相應的17個控制信號需要自己設計邏輯電路實現。硬佈線的邏輯結構如下圖所示:
圖6. 硬佈線控制器結構圖,圖片引用自John D. Carpinelli的《Computer Systems Organization & Architecture》第228頁。
其中計數器有三個動作:LD、INC、CLR,分別表示加載一個新狀態值、狀態值加1、 狀態值清0。這三個動作包含了控制器這個狀態機的全部可能的動作。在表2中,我用S0~S15分別表示狀態0至狀態16,當計算機復位時,計數器值爲0,即爲S0狀態,執行子操作ROM->IR,然後在一個時鐘週期後需要進入一下個狀態S1,執行PC+1->PC,從S0狀態變換到S1狀態只需要向計數器觸發INC動作便可,它就會順序變化下一個狀態。然後下一步就需要加載IR寄存器的高4位操作碼進入計數器,識別是什麼指令,這就需要將IR寄存器的高4位連到計數器的輸入口(圖中Input處),並向計數器觸發LD動作便可,計數器就會順利加載操作碼。計數器當前的狀態值會從右側輸出口輸出至譯碼器,譯碼器會將4位二進制值變爲對應的16位二進制值,這個16位二進制值只有其中1個位爲0,其餘15位均爲1。比如當計數器當前狀態值爲1001,即十進制數字9時,譯碼器輸出端第9位就會輸出0,其餘15位均輸出1。對應的這個第9位,我們就可以根據表2中設計的各部件控制信號進行輸出。當一條指令的最後一個子操作執行完成後,比如表2中IN T指令最後子操作對應的是狀態S3,那麼下一個狀態就應該回到狀態S0,以便進行下一輪取指和譯碼執行。因此,在S3狀態執行完後,我們需要觸發計數器的CLR動作,讓其清0,回到狀態S0。
Gater8的設計中計數器用一片70X163芯片,譯碼器爲一片4位至16位的70X154芯片。剩下的生成邏輯控制信號部分是完全是根據Gater8的具體特點設計的,不可能有現成芯片可用,我們需要用一些與門和或門來實現,這可以用70X08芯片提供與門和70X32芯片提供或門。
下面分析這個邏輯控制電路的設計。我們從一個簡單的控制信號ALUM入手,在前面已經說明,因爲我們的電路設計都是0使能部件,1不使能(禁止)部件。查閱表2中ALUM那一列,發現只有S6和S9兩個狀態下時ALUM爲0,因此我們只要當前狀態爲S6和S9兩者之一時就需要向ALUM這個控制端口發送一個0,其它時候可以不發送,因爲X可以當作1也禁用部件。這樣我們只需用一個與門連接譯碼器的S6和S9端口,這個與門的輸出連接ALUM控制端口就行了,這樣就完成了ALUM的控制信號的設計。用同樣的方法,根據表2,可以完成其它16個控制端口的邏輯設計。其中Aen這個端口很多狀態都會使能它(即給Aen端口置0),而且有些是有條件使能,這時我們可以用K-map或布爾邏輯計算出某一狀態下使能Aen的電路,並將它與其它狀態下的使能Aen的電路與在一起,最終得到完整的Aen的使能電路。例如表2中,狀態S3下使能Aen的電路是S3+OPC,這裏'+'表示布爾或,而狀態S4時是無條件直接使能Aen,它們都一起與在一起共同輸出Aen控制信號。
另外Gater8中,在且僅在狀態S1後就需要加載操作碼,於是只有譯碼器的S1輸出連接到了計數器的LOAD口。而11條指令分別在狀態S2, S3, S5, S6, S8, S9, S10, S11, S13, S14, S15後執行結束,於是這些指令與在一起並連接到了計數器的CLR端口。一旦這11個狀態中有一個值爲0,就表示當前指令執行完畢,需要清計數器爲0,準備處理下一條指令。
至此,複雜的控制器的設計講解完了。
八、關於復位、時鐘信號
關於Gater8的復位,我設計了RST信號,它用於連接在每一個寄存器、計數器的CLR端口,一旦RST信號爲0,這些連接的部件都會清0。不過我對於控制器裏使用的計數器芯片不同於PC計數器的芯片,前者是70X163,它是同步清0,而後者是70X161芯片,它是異步清0。我其它寄存器用的都是70X825芯片,它也是異步清0的。所謂異步清0是指我只要在這些芯片的CLR端口置0,它們就會立即清0生效,無需等待下一個CLK時鐘週期讓其清0生效。而同步清0則需要一個在CLK端口的時鐘信號才能讓清0生效。
我用了163芯片作爲控制器的計數器,讓它清0,需要先置RST爲0,然後產生一個CLK週期,這樣163芯片清0的同時,其它所有的寄存器和計數器也清0了。因爲RST信號未來將連接到一個實體按鈕上,我將設計按鈕按下去產生RST信號0,放開時產生RST信號1,由於人工按鍵的時延,再加上時鐘信號的高速,在人按下和放開中間,必然會有至少1個CLK時鐘週期,因此所有的部件將都能成功清0,完成復位的功能。
爲了設計時測試電路的方使,圖1中Gater8的時鐘信號我是連着一個二進制開關的。這意味着,產生一個時鐘週期,需要人工撥動開關兩次。
你也可以不用二進制開關,換用時鐘信號直接連上去,除了速度變得很快以外,效果一模一樣。但測試電路階段,不利於觀察。
至此,整個邏輯電路也分析完畢。下面進行編程測試Gater8這個自制CPU是否能正確運行程序。
九、編寫程序測試CPU
終於到了寫程序的階段了, 這一部分我們設計一小段程序,並將對應的機器碼導入到ROM芯片內,讓Gater8運行,看是否能正確執行得到預期的結果。
測試程序如下:
LDR 0XFFF ;從RAM的0XFFF地址處讀取1字節到A寄存器
LDI T, #0xFF ;從ROM中讀取立即數0xFF到T寄存器,相當於賦值 T=0xFF
IN A ;從輸入設備DEV0讀取一個字節到A寄存器
OUT DEV1, A ;輸出A的值到DEV1
ADD A ;計算A=A+T
LDI T, #0x01 ;賦值T=0x01
Loop:
SUB A ;計算A=A-1
JNZ Loop ;如果A≠0就跳到Loop處執行
LDI A, #0x55 ;A=0x55
LDI T, #0x0E ;T=0x0E
ADD A ;A=A+T=0x55+0x0E=0x63
OR T ;T=A | T=0x63 | 0x0E=0x6F
OUT DEV2, T ;輸出T的值0x6F到DEV2
STR 0x010 ;保存A的值0x63到RAM的地址0x010處
Stop:
JMP Stop ;程序執行結束,執行無限循環
由於我還沒有編寫好彙編編譯器程序,先用手工將上面這段程序翻譯成機器碼,並導入到ROM芯片中,如下圖所示:
然後復位Gater8(即RST置0,然後CLK撥動兩次,再RST置1),不斷撥動CLK二進制開關產生時鐘信號,並觀察每個寄存器、Zflag、控制器狀態等值的變化,以及輸出設備DEV1和DEV2的LED點亮情況。前面圖2給出了當輸入設備DEV0值爲0x04時,程序最終運行狀態截圖,可以看到DEV1的LED燈顯示值爲0x04,而DEV2的LED燈顯示值爲0x6F。圖8給出了輸入輸出局部運行結果截圖。
圖8. 輸入輸出部分運行結果截圖。
並且,程序最後停在最後這條機器碼爲A0的指令(就是程序中最後的那條JMP指令)處進行無限循環,一切如預期一樣正常,中途觀察各寄存器的值變化也都正確。最後STR寫入RAM處的值如圖9顯示,也都正確:
圖9. 程序中A寄存器的值0x63正確寫入RAM的0x010處。
上面的演示程序雖然簡單,但演示了所設計的指令。雖然指令集不大,但它是精心設計的,可以充分利用這些指令寫出功能更加強大的程序。
十、小結
這個自己設計的Gater8,前前後後被不斷修改了很多次,至少目前的這是第3個版本。指令集也是精心挑選,最終確定了11條。整個過程不但可以用來設計8位自己的CPU,同時也可以用來設計16位和32位,甚至64位CPU,它們的原理是一樣的。
設計邏輯電路本身並不太複雜,可以輕鬆根據Gater8設計出另一個Gater16或Gater32,但後期用過多的芯片,擔心會很難搭建出來。畢竟在軟件裏設計實驗成功的Gater8在實際搭建階段還可能會遇到一些不確定的問題。所以在邏輯設計階段,我已儘可能精簡電路,並同時保障一定的功能的自由性。
本來考慮用5位操作碼,提供32個狀態,這樣可以實現更多的指令。而且控制器中的計數器和譯碼器的連接部分也完成了設計,見圖10。後來也是考慮到(1)過多與門芯片和或門芯片問題,(2)內存地址線相應少一根,4KB的地址空間將變爲2KB,所以暫時還是保持當前4位操作碼的設計。
圖10. 用5位操作碼錶示32個狀態。
接下來就是關鍵的7400系列芯片搭建,當然還有彙編器的編寫,敬請期待。