程式人雜誌 -- 2013 年 12 月號 (開放公益出版品)

開放電腦計畫 (6) – 一顆只有 51 行 Verilog 程式碼的 16 位元處理器 MCU0 (作者:陳鍾誠)

簡介

在前兩期當中,我們設計出了出了CPU0 處理器的 Verilog 程式碼 ,文章網址如下:

如果您讀過上述文章,應該已經瞭解如何用 Verilog 設計 CPU 了。

但是、 CPU0 雖然已經設計的很簡單了,但其實我們還可以設計出更加簡單的處理器。在本期中、我們將提出一個 更簡易的 16 位元 CPU 微控制器,稱為 MCU0。

MCU0 的架構

MCU0 是一顆 16 位元的 CPU,所有暫存器都是 16 位元的,總共有 (IR, SW, PC, A) 等四個暫存器,如下所示:

暫存器名稱 功能 說明
IR 指令暫存器 用來儲存從記憶體載入的機器碼指令
PC 程式計數器 用來儲存指令的位址 (也就是目前執行到哪個指令的記憶體位址)
SW 狀態暫存器 用來儲存 CMP 比較指令的結果旗標,像是負旗標 N 與零旗標 Z 等。作為條件跳躍 JEQ 等指令是否跳躍的判斷依據。
A 累積器 用來儲存計算的結果,像是加減法的結果。

為了讓程式極度簡化,在本文中我們只實作測試程式所用到的「必要指令」,總共有 6 個,如下所示:

代碼 名稱 格式 說明 語意
0 LD LD C 載入 A = [C]
1 ADD ADD C 加法 A = A + [C]
2 JMP JMP C 跳躍 PC = C
3 ST ST C 儲存 [C] = A
4 CMP CMP C 比較 SW = A CMP [C]
5 JEQ JEQ C 相等時跳躍 if SW[30]=Z=1 then PC = C

組合語言與機器碼

以下是一個 MCU0 處理器的組合語言範例程式,該程式可以計算從 SUM=1+2+...+N 的結果。 其中第一欄是指令或資料的機器碼,註解符號 // 之後則是位址與組合語言。

檔案: cpu16m.hex

00 16  // 00    LOOP:   LD    I    
40 1A  // 02            CMP   N    
50 12  // 04            JEQ   EXIT
10 18  // 06            ADD   K1    
30 16  // 08            ST    I    
00 14  // 0A            LD    SUM    
10 16  // 0C            ADD   I    
30 14  // 0E            ST    SUM    
20 00  // 10            JMP   LOOP
20 12  // 12    EXIT:   JMP   EXIT
00 00  // 14    SUM:    WORD  0    
00 00  // 16    I:      WORD  0    
00 01  // 18    K1:     WORD  1    
00 0A  // 1A    N:      WORD  10     

MCU0 的指令格式很簡單,當指令被載入指令暫存器 IR 後,前四個位元 IR[15:12] 是指令代碼 (OP),而 後 12 個位元 IR[11:0] 則是一個常數 C,該常數通常代表記憶體位址。(由於採用絕對定址,所以 MCU0 的 記憶體最大只能達 2 的 12 次方,也就是從 0 到 4095。

由於指令格式只有一種,分為 4 位元的 OP 代碼與 12 位元的 C 常數,因此編碼非常容易,例如 LD I 這個指令, 由於 LD 的代碼為 0,而 I 的位址為 0x16,所以整個指令編碼為 0016。而對於 CMP N 這個指令而言,由於 CMP 的代碼為 4,變數 N 的位址為 0x1A,所以整個指令編碼為 401A。

Verilog 程式實作

於是、整個 mcu0m 處理器只有短短的 51 行 Verilog 程式就實作完了,以下是全部的程式碼列表。

檔案: mcu0m.v

`define N    SW[15] // 負號旗標
`define Z    SW[14] // 零旗標
`define OP   IR[15:12] // 運算碼
`define C    IR[11:0]  // 常數欄位
`define M    {m[`C], m[`C+1]}

module cpu(input clock); // CPU0-Mini 的快取版:cpu0mc 模組
  parameter [3:0] LD=4'h0,ADD=4'h1,JMP=4'h2,ST=4'h3,CMP=4'h4,JEQ=4'h5;
  reg signed [15:0] A;   // 宣告暫存器 R[0..15] 等 16 個 32 位元暫存器
  reg [15:0] IR;  // 指令暫存器 IR
  reg [15:0] SW;  // 指令暫存器 IR
  reg [15:0] PC;  // 程式計數器
  reg [15:0] pc0;
  reg [7:0]  m [0:32];    // 內部的快取記憶體
  integer i;  
  initial  // 初始化
  begin
    PC = 0; // 將 PC 設為起動位址 0
    SW = 0;
    $readmemh("mcu0m.hex", m);
    for (i=0; i < 32; i=i+2) begin
       $display("%8x: %8x", i, {m[i], m[i+1]});
    end
  end
  
  always @(posedge clock) begin // 在 clock 時脈的正邊緣時觸發
    IR = {m[PC], m[PC+1]};  // 指令擷取階段:IR=m[PC], 2 個 Byte 的記憶體
    pc0= PC;                // 儲存舊的 PC 值在 pc0 中。
    PC = PC+2;              // 擷取完成,PC 前進到下一個指令位址
    case (`OP)              // 解碼、根據 OP 執行動作
      LD: A = `M;         // LD C
      ST: `M = A;             // ST C
      CMP: begin `N=(A < `M); `Z=(A==`M); end // CMP C
      ADD: A = A + `M;    // ADD C
      JMP: PC = `C;       // JMP C
      JEQ: if (`Z) PC=`C;     // JEQ C
    endcase
    // 印出 PC, IR, SW, A 等暫存器值以供觀察
    $display("%4dns PC=%x IR=%x, SW=%x, A=%d", $stime, pc0, IR, SW, A); 
  end
endmodule

module main;                // 測試程式開始
reg clock;                  // 時脈 clock 變數

cpu cpux(clock);            // 宣告 cpu0mc 處理器

initial clock = 0;          // 一開始 clock 設定為 0
always #10 clock=~clock;    // 每隔 10ns 反相,時脈週期為 20ns
initial #2000 $finish;      // 在 2000 奈秒的時候停止測試。
endmodule

執行結果

上述程式以 mcu0m.hex 這個 16 進位的機器碼檔作為輸入,其編譯執行結果如下:

C:\Dropbox\Public\web\oc\code\mcu>iverilog -o mcu0m mcu0m.v

C:\Dropbox\Public\web\oc\code\mcu>vvp mcu0m
WARNING: mcu0m.v:20: $readmemh(mcu0m.hex): Not enough words in the file for the
requested range [0:32].
00000000:     0016
00000002:     401a
00000004:     5012
00000006:     1018
00000008:     3016
0000000a:     0014
0000000c:     1016
0000000e:     3014
00000010:     2000
00000012:     2012
00000014:     0000
00000016:     0000
00000018:     0001
0000001a:     000a
0000001c:     xxxx
0000001e:     xxxx
  10ns PC=0000 IR=0016, SW=0000, A=     0
  30ns PC=0002 IR=401a, SW=8000, A=     0
  50ns PC=0004 IR=5012, SW=8000, A=     0
  70ns PC=0006 IR=1018, SW=8000, A=     1
  90ns PC=0008 IR=3016, SW=8000, A=     1
 110ns PC=000a IR=0014, SW=8000, A=     0
 130ns PC=000c IR=1016, SW=8000, A=     1
 150ns PC=000e IR=3014, SW=8000, A=     1
 170ns PC=0010 IR=2000, SW=8000, A=     1
 190ns PC=0000 IR=0016, SW=8000, A=     1
 210ns PC=0002 IR=401a, SW=8000, A=     1
 230ns PC=0004 IR=5012, SW=8000, A=     1
 250ns PC=0006 IR=1018, SW=8000, A=     2
 270ns PC=0008 IR=3016, SW=8000, A=     2
 290ns PC=000a IR=0014, SW=8000, A=     1
 310ns PC=000c IR=1016, SW=8000, A=     3
 330ns PC=000e IR=3014, SW=8000, A=     3
 350ns PC=0010 IR=2000, SW=8000, A=     3
 370ns PC=0000 IR=0016, SW=8000, A=     2
 390ns PC=0002 IR=401a, SW=8000, A=     2
 410ns PC=0004 IR=5012, SW=8000, A=     2
 430ns PC=0006 IR=1018, SW=8000, A=     3
 450ns PC=0008 IR=3016, SW=8000, A=     3
 470ns PC=000a IR=0014, SW=8000, A=     3
 490ns PC=000c IR=1016, SW=8000, A=     6
 510ns PC=000e IR=3014, SW=8000, A=     6
 530ns PC=0010 IR=2000, SW=8000, A=     6
 550ns PC=0000 IR=0016, SW=8000, A=     3
 570ns PC=0002 IR=401a, SW=8000, A=     3
 590ns PC=0004 IR=5012, SW=8000, A=     3
 610ns PC=0006 IR=1018, SW=8000, A=     4
 630ns PC=0008 IR=3016, SW=8000, A=     4
 650ns PC=000a IR=0014, SW=8000, A=     6
 670ns PC=000c IR=1016, SW=8000, A=    10
 690ns PC=000e IR=3014, SW=8000, A=    10
 710ns PC=0010 IR=2000, SW=8000, A=    10
 730ns PC=0000 IR=0016, SW=8000, A=     4
 750ns PC=0002 IR=401a, SW=8000, A=     4
 770ns PC=0004 IR=5012, SW=8000, A=     4
 790ns PC=0006 IR=1018, SW=8000, A=     5
 810ns PC=0008 IR=3016, SW=8000, A=     5
 830ns PC=000a IR=0014, SW=8000, A=    10
 850ns PC=000c IR=1016, SW=8000, A=    15
 870ns PC=000e IR=3014, SW=8000, A=    15
 890ns PC=0010 IR=2000, SW=8000, A=    15
 910ns PC=0000 IR=0016, SW=8000, A=     5
 930ns PC=0002 IR=401a, SW=8000, A=     5
 950ns PC=0004 IR=5012, SW=8000, A=     5
 970ns PC=0006 IR=1018, SW=8000, A=     6
 990ns PC=0008 IR=3016, SW=8000, A=     6
1010ns PC=000a IR=0014, SW=8000, A=    15
1030ns PC=000c IR=1016, SW=8000, A=    21
1050ns PC=000e IR=3014, SW=8000, A=    21
1070ns PC=0010 IR=2000, SW=8000, A=    21
1090ns PC=0000 IR=0016, SW=8000, A=     6
1110ns PC=0002 IR=401a, SW=8000, A=     6
1130ns PC=0004 IR=5012, SW=8000, A=     6
1150ns PC=0006 IR=1018, SW=8000, A=     7
1170ns PC=0008 IR=3016, SW=8000, A=     7
1190ns PC=000a IR=0014, SW=8000, A=    21
1210ns PC=000c IR=1016, SW=8000, A=    28
1230ns PC=000e IR=3014, SW=8000, A=    28
1250ns PC=0010 IR=2000, SW=8000, A=    28
1270ns PC=0000 IR=0016, SW=8000, A=     7
1290ns PC=0002 IR=401a, SW=8000, A=     7
1310ns PC=0004 IR=5012, SW=8000, A=     7
1330ns PC=0006 IR=1018, SW=8000, A=     8
1350ns PC=0008 IR=3016, SW=8000, A=     8
1370ns PC=000a IR=0014, SW=8000, A=    28
1390ns PC=000c IR=1016, SW=8000, A=    36
1410ns PC=000e IR=3014, SW=8000, A=    36
1430ns PC=0010 IR=2000, SW=8000, A=    36
1450ns PC=0000 IR=0016, SW=8000, A=     8
1470ns PC=0002 IR=401a, SW=8000, A=     8
1490ns PC=0004 IR=5012, SW=8000, A=     8
1510ns PC=0006 IR=1018, SW=8000, A=     9
1530ns PC=0008 IR=3016, SW=8000, A=     9
1550ns PC=000a IR=0014, SW=8000, A=    36
1570ns PC=000c IR=1016, SW=8000, A=    45
1590ns PC=000e IR=3014, SW=8000, A=    45
1610ns PC=0010 IR=2000, SW=8000, A=    45
1630ns PC=0000 IR=0016, SW=8000, A=     9
1650ns PC=0002 IR=401a, SW=8000, A=     9
1670ns PC=0004 IR=5012, SW=8000, A=     9
1690ns PC=0006 IR=1018, SW=8000, A=    10
1710ns PC=0008 IR=3016, SW=8000, A=    10
1730ns PC=000a IR=0014, SW=8000, A=    45
1750ns PC=000c IR=1016, SW=8000, A=    55
1770ns PC=000e IR=3014, SW=8000, A=    55
1790ns PC=0010 IR=2000, SW=8000, A=    55
1810ns PC=0000 IR=0016, SW=8000, A=    10
1830ns PC=0002 IR=401a, SW=4000, A=    10
1850ns PC=0004 IR=5012, SW=4000, A=    10
1870ns PC=0012 IR=2012, SW=4000, A=    10
1890ns PC=0012 IR=2012, SW=4000, A=    10
1910ns PC=0012 IR=2012, SW=4000, A=    10
1930ns PC=0012 IR=2012, SW=4000, A=    10
1950ns PC=0012 IR=2012, SW=4000, A=    10
1970ns PC=0012 IR=2012, SW=4000, A=    10
1990ns PC=0012 IR=2012, SW=4000, A=    10

您可以看到在 1750ns 的時候,程式執行到 PC=000C 這行,也就是下列的 ADD I,計算出了 1+2+...+10 的結果, 也就是 55,然後 ST SUM 將 55 存入 SUM 中,接著 JMP LOOP 跳回 PC=0000 的 LOOP: LD I 繼續執行,然後 CMP N 指令將載入到 A 暫存器的 I 值 (10) 與 N 的值 (10) 作比較,於是在 JEQ 指令時由於兩者已經相等,於是就跳到 EXIT 標記的 0012 去執行。

但是位於 EXIT 的指令是 JMP EXIT,也就是一個無窮迴圈程式,因此程式會不斷在 0012 這個位址重複執行,直到 2000ns 的時候被 $finish 強制結束。

00 16  // 00    LOOP:   LD    I    
40 1A  // 02            CMP   N    
50 12  // 04            JEQ   EXIT
10 18  // 06            ADD   K1    
30 16  // 08            ST    I    
00 14  // 0A            LD    SUM    
10 16  // 0C            ADD   I    
30 14  // 0E            ST    SUM    
20 00  // 10            JMP   LOOP
20 12  // 12    EXIT:   JMP   EXIT
00 00  // 14    SUM:    WORD  0    
00 00  // 16    I:      WORD  0    
00 01  // 18    K1:     WORD  1    
00 0A  // 1A    N:      WORD  10     

結語

雖然在前兩期當中,我們已經設計了一顆完整的 32 位元處理器 CPU0,但是該處理器的指令格式有三種, 即使設計已經相當簡單,但是筆者發現還是有些讀者無法理解,因此在本期當中我們又設計了一顆更簡單的 16 位元處理器 MCU0,並且實作了其中 6 個指令,完成了一個可以進行 SUM=1+2+...+N 的程式,希望透過 這樣的簡化,可以讓讀者們更容易理解一顆 CPU 的設計原理。

在本學期筆者教授計算機結構課程的過程當中,我發現透過 16 位元 MCU0 的對照,可以更容易的讓學生瞭解 處理器的設計方式,因為若只教 32 位元處理器 CPU0 的設計,學生很難理解還有甚麼樣的方式可以設計 「另一種指令集與 CPU」,但是加入了 MCU0 之後,由於有 「16 位元 vs. 32 位元」、「單一指令格式 vs. 多種指令格式」 與「單一累積暫存器 vs. 16 個通用暫存器」的區別,學生們會更容易瞭解不同設計背後的優缺點,也才能有 足夠的背景知識可以完成他們的期末作業 -- 「自己設計一顆處理器的指令集、Verilog 程式,並用 Icarus 進行測試」 的任務。