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

開放電腦計畫 (7) – 完整指令集的 16 位元處理器 MCU0s (作者:陳鍾誠)

在上期當中,我們設計出了出了一顆非常簡易的 16 位元微控制器 MCU0m (MCU0-Mini),其文章網址如下:

但是上述文章中的「微控制器」只包含六個指令,是不夠完整的,因此在本期當中,我們將擴充 MCU0m 成為一個完整的處理器 MCU0s (MCU0-Simple)。

MCU0 的架構

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

`define A    R[0]      // 累積器
`define LR   R[1]      // 狀態暫存器
`define SW   R[2]      // 狀態暫存器
`define SP   R[3]      // 堆疊暫存器
`define PC   R[4]      // 程式計數器

這些暫存器的功能與說明如下:

暫存器名稱 功能 說明
IR 指令暫存器 用來儲存從記憶體載入的機器碼指令
A =R[0] 累積器 用來儲存計算的結果,像是加減法的結果。
LR=R[1] 連結暫存器 用來儲存函數呼叫的返回位址
SW=R[2] 狀態暫存器 用來儲存 CMP 比較指令的結果旗標,像是負旗標 N 與零旗標 Z 等。作為條件跳躍 JEQ 等指令是否跳躍的判斷依據。
SP=R[3] 堆疊暫存器 堆疊指標,PUSH, POP 指令會用到。
PC=R[4] 程式計數器 用來儲存指令的位址 (也就是目前執行到哪個指令的記憶體位址)

MCU0 的指令表

指令暫存器 IR 的前 4 個位元是指令代碼 OP,由於 4 位元只能表達 16 種指令,這數量太少不敷使用,因此當 OP=0xF 時, 我們繼續用後面的位元作為延伸代碼,以便有更多的指令可以使用,以下是 MCU0 微控制器的完整指令表。

代碼 名稱 格式 說明 語意
0 LD LD C 載入 A = [C]
1 ST ST C 儲存 [C] = A
2 ADD ADD C 加法 A = A + [C]
3 SUB SUB C 減法 A = A - [C]
4 MUL MUL C 乘法 A = A * [C]
5 DIV DIV C 除法 A = A / [C]
6 AND AND C 位元 AND 運算 A = A & [C]
7 OR OR C 位元 OR 運算 A = A | [C]
8 XOR XOR C 位元 XOR 運算 A = A ^ [C]
9 CMP CMP C 比較 SW = A CMP [C] ; N=(A<[C]), Z=(A==[C])
A JMP JMP C 跳躍 PC = C
B JEQ JEQ C 相等時跳躍 if Z then PC = C
C JLT JLT C 小於時跳躍 if N then PC = C
D JLE JLE C 小於或等於時跳躍 if Z or N then PC = C
E CALL CALL C 呼叫副程式 LR=PC; PC = C
F OP8 OP為8位元的運算
F0 LDI LDI Ra,C4 載入常數 Ra=C4
F2 MOV MOV Ra,Rb 暫存器移動 Ra=Rb
F3 PUSH PUSH Ra 堆疊推入 SP--; [SP] = Ra
F4 POP POP Ra 堆疊取出 Ra=[SP]; SP++;
F5 SHL SHL Ra,C4 左移 Ra = Ra << C4
F6 SHR SHL Ra,C4 右移 Ra = Ra >> C4
F7 ADDI ADDI Ra,C4 常數加法 Ra = Ra + C4
F8 SUBI SUBI Ra,C4 常數減法 Ra = Ra - C4
F9 NEG NEG Ra 反相 Ra = ~Ra
FA SWI SWI C 軟體中斷 BIOS 中斷呼叫
FD NSW NSW 狀態反相 N=~N, Z=~Z; 由於沒有 JGE, JGT, JNE,因此可用此指令將 SW 反相,再用 JLE, JLT, JEQ 完成跳躍動作
FE RET RET 返回 PC = LR
FF IRET IRET 從中斷返回 PC = LR; I=0;

MCU0 程式碼

檔案:mcu0s.v

`define OP   IR[15:12] // 運算碼
`define C    IR[11:0]  // 常數欄位
`define SC8  $signed(IR[7:0]) // 常數欄位
`define C4   IR[3:0]   // 常數欄位
`define Ra   IR[7:4]   // Ra
`define Rb   IR[3:0]   // Rb
`define A    R[0]      // 累積器
`define LR   R[1]      // 狀態暫存器
`define SW   R[2]      // 狀態暫存器
`define SP   R[3]      // 堆疊暫存器
`define PC   R[4]      // 程式計數器
`define N    `SW[15]   // 負號旗標
`define Z    `SW[14]   // 零旗標
`define I    `SW[3]    // 是否中斷中
`define M    m[`C]     // 存取記憶體

module cpu(input clock); // CPU0-Mini 的快取版:cpu0mc 模組
  parameter [3:0] LD=4'h0,ST=4'h1,ADD=4'h2,SUB=4'h3,MUL=4'h4,DIV=4'h5,AND=4'h6,OR=4'h7,XOR=4'h8,CMP=4'h9,JMP=4'hA,JEQ=4'hB, JLT=4'hC, JLE=4'hD, JSUB=4'hE, OP8=4'hF;
  parameter [3:0] LDI=4'h0, MOV=4'h2, PUSH=4'h3, POP=4'h4, SHL=4'h5, SHR=4'h6, ADDI=4'h7, SUBI=4'h8, NEG=4'h9, SWI=4'hA, NSW=4'hD, RET=4'hE, IRET=4'hF;
  reg [15:0] IR;    // 指令暫存器
  reg signed [15:0] R[0:4];
  reg signed [15:0] pc0;
  reg signed [15:0] m [0:4096]; // 內部的快取記憶體
  integer i;
  initial  // 初始化
  begin
    `PC = 0; // 將 PC 設為起動位址 0
    `SW = 0;
    $readmemh("mcu0s.hex", m);
  end
  
  always @(posedge clock) begin // 在 clock 時脈的正邊緣時觸發
    IR = m[`PC];                // 指令擷取階段:IR=m[PC], 2 個 Byte 的記憶體
    pc0= `PC;                   // 儲存舊的 PC 值在 pc0 中。
    `PC = `PC+1;                // 擷取完成,PC 前進到下一個指令位址
    case (`OP)                  // 解碼、根據 OP 執行動作
      LD: `A = `M;                 // LD C
      ST: `M = `A;                // ST C
      ADD: `A = `A + `M;        // ADD C
      SUB: `A = `A - `M;        // SUB C
      MUL: `A = `A * `M;        // MUL C
      DIV: `A = `A / `M;        // DIV C
      AND: `A = `A & `M;        // AND C
      OR : `A = `A | `M;        // OR  C
      XOR: `A = `A ^ `M;        // XOR C
      CMP: begin `N=(`A < `M); `Z=(`A==`M); end // CMP C
      JMP: `PC = `C; // JSUB C
      JEQ: if (`Z) `PC=`C;        // JEQ C
      JLT: if (`N) `PC=`C;        // JLT C
      JLE: if (`N || `Z) `PC=`C;// JLE C
      JSUB:begin `LR = `PC; `PC = `C; end // JSUB C
      OP8: case (IR[11:8])      // OP8: 加長運算碼
        LDI:  R[`Ra] = `C4;                         // LDI C
        ADDI: R[`Ra] = R[`Ra] + `C4;                // ADDI C
        SUBI: R[`Ra] = R[`Ra] - `C4;                // ADDI C
        MOV:  R[`Ra] = R[`Rb];                      // MOV Ra, Rb
        PUSH: begin `SP=`SP-1; m[`SP] = R[`Ra]; end // PUSH Ra
        POP:  begin R[`Ra] = m[`SP]; `SP=`SP+1; end // POP  Ra
        SHL:  R[`Ra] = R[`Ra] << `C4;               // SHL C
        SHR:  R[`Ra] = R[`Ra] >> `C4;               // SHR C
        SWI:  $display("SWI C8=%d A=%d", `SC8, `A); // SWI C
        NEG:  R[`Ra] = ~R[`Ra];                     // NEG Ra
        NSW:  begin `N=~`N; `Z=~`Z; end             // NSW  (negate N, Z)
        RET:  `PC = `LR;                            // RET
        IRET: begin `PC = `LR; `I = 0; end          // IRET
        default: $display("op8=%d , not defined!", IR[11:8]);
      endcase
    endcase
    // 印出 PC, IR, SW, A 等暫存器值以供觀察
    $display("%4dns PC=%x IR=%x, SW=%x, A=%d SP=%x LR=%x", $stime, pc0, IR, `SW, `A, `SP, `LR);
  end
endmodule

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

cpu mcu0(clock);            // 宣告 mcu0 處理器

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

endmodule

組合語言

檔案:mcu0s.hex

0020  // 00    RESET:  LD    X   
2021  // 01            ADD   Y
3021  // 02            SUB   Y
4021  // 03            MUL   Y
5021  // 04            DIV   Y
7021  // 05            OR    Y             
6021  // 06            AND   Y
8021  // 07            XOR   Y
0020  // 08            LD    X
F503  // 09            SHL   A, 3
F603  // 0A            SHR   A, 3
F701  // 0B            ADDI  1
0023  // 0C            LD    STACKEND
F230  // 0D            MOV   SP, A
E011  // 0E            JSUB  MIN
0022  // 0F            LD    Z
A010  // 10    HALT:   JMP   HALT        
F301  // 11    MIN:    PUSH  LR
0020  // 12            LD    X
9021  // 13            CMP   Y
FD00  // 14            NSW
C018  // 15            JLT   ELSE
1022  // 16            ST    Z
A019  // 17            JMP   NEXT
0021  // 18    ELSE:   LD    Y
1022  // 19    NEXT:   ST    Z
F401  // 1A            POP   LR
FE00  // 1B            RET
0000  // 1C    
0000  // 1D    
0000  // 1E
0000  // 1F
0003  // 20    X:      WORD  3
0005  // 21    Y:      WORD  5
0000  // 22    Z:      WORD  0 
007F  // 23    STACKEND: WORD 127

執行結果

D:\Dropbox\Public\web\oc\code\mcu0>iverilog -o mcu0s mcu0s.v

D:\Dropbox\Public\web\oc\code\mcu0>vvp mcu0s
WARNING: mcu0s.v:29: $readmemh(mcu0s.hex): Not enough words in the file for the
requested range [0:4096].
  10ns PC=0000 IR=0020, SW=0000, A=    3 SP=xxxx LR=xxxx
  30ns PC=0001 IR=2021, SW=0000, A=    8 SP=xxxx LR=xxxx
  50ns PC=0002 IR=3021, SW=0000, A=    3 SP=xxxx LR=xxxx
  70ns PC=0003 IR=4021, SW=0000, A=   15 SP=xxxx LR=xxxx
  90ns PC=0004 IR=5021, SW=0000, A=    3 SP=xxxx LR=xxxx
 110ns PC=0005 IR=7021, SW=0000, A=    7 SP=xxxx LR=xxxx
 130ns PC=0006 IR=6021, SW=0000, A=    5 SP=xxxx LR=xxxx
 150ns PC=0007 IR=8021, SW=0000, A=    0 SP=xxxx LR=xxxx
 170ns PC=0008 IR=0020, SW=0000, A=    3 SP=xxxx LR=xxxx
 190ns PC=0009 IR=f503, SW=0000, A=   24 SP=xxxx LR=xxxx
 210ns PC=000a IR=f603, SW=0000, A=    3 SP=xxxx LR=xxxx
 230ns PC=000b IR=f701, SW=0000, A=    4 SP=xxxx LR=xxxx
 250ns PC=000c IR=0023, SW=0000, A=  127 SP=xxxx LR=xxxx
 270ns PC=000d IR=f230, SW=0000, A=  127 SP=007f LR=xxxx
 290ns PC=000e IR=e011, SW=0000, A=  127 SP=007f LR=000f
 310ns PC=0011 IR=f301, SW=0000, A=  127 SP=007e LR=000f
 330ns PC=0012 IR=0020, SW=0000, A=    3 SP=007e LR=000f
 350ns PC=0013 IR=9021, SW=8000, A=    3 SP=007e LR=000f
 370ns PC=0014 IR=fd00, SW=4000, A=    3 SP=007e LR=000f
 390ns PC=0015 IR=c018, SW=4000, A=    3 SP=007e LR=000f
 410ns PC=0016 IR=1022, SW=4000, A=    3 SP=007e LR=000f
 430ns PC=0017 IR=a019, SW=4000, A=    3 SP=007e LR=000f
 450ns PC=0019 IR=1022, SW=4000, A=    3 SP=007e LR=000f
 470ns PC=001a IR=f401, SW=4000, A=  127 SP=007f LR=000f
 490ns PC=001b IR=fe00, SW=4000, A=  127 SP=007f LR=000f
 510ns PC=000f IR=0022, SW=4000, A=    3 SP=007f LR=000f
 530ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 550ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 570ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 590ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 610ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 630ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 650ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 670ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 690ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 710ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 730ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 750ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 770ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 790ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 810ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 830ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 850ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 870ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 890ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 910ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 930ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 950ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 970ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f
 990ns PC=0010 IR=a010, SW=4000, A=    3 SP=007f LR=000f

結語

由於 16 位元處理器的指令長度很短,因此空間必須有效利用,所以我們將一些不包含記憶體位址的指令, 編到最後的 0xF 的 OP 代碼當中,這樣就可以再度延伸出一大群指令空間,於是讓指令數可以不受限於 4 位元 OP 碼的 16 個指令,而能延伸為 30 個左右的指令。

在使用 Verilog 這種硬體描述語言設計處理器時,位元數越少,往往處理器的指令長度越少,這時處理器 不見得會更好設計,往往反而會更難設計,指令集的編碼相對會困難一些。