開放電腦計畫 (4) – CPU0mc 處理器:使用 Verilog 實作 (作者:陳鍾誠)
從本期開始,我們將陸續介紹開放電腦計畫中的處理器,CPU0 的設計方式,我們將使用 Verilog 硬體描述語言來設計 CPU0 處理器。
但是由於處理器的設計比較複雜,若採用一步到位的方式,恐怕讀者會難以理解。因此我們將採用循序漸進的方式,從 只支援 4 個指令的超微小處理器 CPU0-Mini 開始,來解說處理器的設計方式。
在本文中,我們將用最簡單的方式,在完全不考慮成本與實用性的情況之下,設計一個將記憶體內建在 CPU0-Mini 內部 的處理器,稱為 CPU0mc,也就是 CPU0-Mini-Cache 的簡稱。
簡介
還記得我們在 2013 年 6 月號的程式人雜誌中,刊載過的下列文章嗎?這篇文章說明了 CPU0 處理器的架構。
在本文中,我們將從 CPU0 的指令集當中,挑出幾個指令,以便寫出一個可以計算 1+2+.....+n+.... 的組合語言程式 (喔!不、應該說是機器語言程式),然後用 Verilog 實作一個可以執行這些指令的 CPU,這個微小的 CPU 版本稱為 CPU0mc。
我們所挑的幾個指令如下:
格式 | 指令 | OP | 說明 | 語法 | 語意 |
---|---|---|---|---|---|
L | LD | 00 | 載入word | LD Ra, [Rb+Cx] | Ra=[Rb+Cx] |
L | ST | 01 | 儲存word | ST Ra, [Rb+Cx] | Ra=[Rb+Cx] |
A | ADD | 13 | 加法 | ADD Ra, Rb, Rc | Ra=Rb+Rc |
J | JMP | 26 | 跳躍 (無條件) | JMP Cx | PC=PC+Cx |
然後,我們就可以用這幾個指令寫出以下的程式:
位址 | 機器碼 | 標記 | 組合語言 | 對照的 C 語言 |
---|---|---|---|---|
0000 | 001F0018 | LD R1, K1 | R1 = K1 | |
0004 | 002F0010 | LD R2, K0 | R2 = K0 | |
0008 | 003F0014 | LD R3, SUM | R3 = SUM | |
000C | 13221000 | LOOP: | ADD R2, R2, R1 | R2 = R2 + R1 |
0010 | 13332000 | ADD R3, R3, R2 | R3 = R3 + R2 | |
0014 | 26FFFFF4 | JMP LOOP | goto LOOP | |
0018 | 00000000 | K0: | WORD 0 | int K0=0 |
001C | 00000001 | K1: | WORD 1 | int K1=1 |
0020 | 00000000 | SUM: | WORD 0 | int SUM=0 |
這個程式的行為模式,是會讓暫存器 R3 (對應到 SUM) 從 0, 1, 1+2, 1+2+3, .... 一路向上跑,而且是永無止境的無窮迴圈。 因此我們會看到 R3 的內容會是 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55 ... ,的情況。
CPU0mc 模組
以下就是我們所設計的 CPU0mc 模組,以及測試的主程式,我們在程式中寫了詳細的說明,請讀者對照閱讀。
檔案:CPU0mc
`define PC R[15] // 程式計數器 PC 其實是 R[15] 的別名
module cpu0mc(input clock); // CPU0-Mini 的快取版:cpu0mc 模組
parameter [7:0] LD = 8'h00, ST=8'h01, ADD=8'h13, JMP=8'h26; // 支援 4 個指令
reg signed [31:0] R [0:15]; // 宣告暫存器 R[0..15] 等 16 個 32 位元暫存器
reg signed [31:0] IR; // 指令暫存器 IR
reg [7:0] m [0:128]; // 內部的快取記憶體
reg [7:0] op; // 變數:運算代碼 op
reg [3:0] ra, rb, rc; // 變數:暫存器代號 ra, rb, rc
reg signed [11:0] cx12; // 變數:12 位元常數 cx12
reg signed [15:0] cx16; // 變數:16 位元常數 cx16
reg signed [23:0] cx24; // 變數:24 位元常數 cx24
reg signed [31:0] addr; // 變數:暫存記憶體位址
initial // 初始化
begin
`PC = 0; // 將 PC 設為起動位址 0
R[0] = 0; // 將 R[0] 暫存器強制設定為 0
{m[0],m[1],m[2],m[3]} = 32'h001F0018; // 0000 LD R1, K1
{m[4],m[5],m[6],m[7]} = 32'h002F0010; // 0004 LD R2, K0
{m[8],m[9],m[10],m[11]} = 32'h003F0014; // 0008 LD R3, SUM
{m[12],m[13],m[14],m[15]}= 32'h13221000; // 000C LOOP: ADD R2, R2, R1
{m[16],m[17],m[18],m[19]}= 32'h13332000; // 0010 ADD R3, R3, R2
{m[20],m[21],m[22],m[23]}= 32'h26FFFFF4; // 0014 JMP LOOP
{m[24],m[25],m[26],m[27]}= 32'h00000000; // 0018 K0: WORD 0
{m[28],m[29],m[30],m[31]}= 32'h00000001; // 001C K1: WORD 1
{m[32],m[33],m[34],m[35]}= 32'h00000000; // 0020 SUM: WORD 0
end
always @(posedge clock) begin // 在 clock 時脈的正邊緣時觸發
IR = {m[`PC], m[`PC+1], m[`PC+2], m[`PC+3]}; // 指令擷取階段:IR=m[PC], 4 個 Byte 的記憶體
`PC = `PC+4; // 擷取完成,PC 前進到下一個指令位址
{op,ra,rb,rc,cx12} = IR; // 解碼階段:將 IR 解為 {op, ra, rb, rc, cx12}
cx24 = IR[23:0]; // 解出 IR[23:0] 放入 cx24
cx16 = IR[15:0]; // 解出 IR[15:0] 放入 cx16
addr = `PC+cx16; // 記憶體存取位址 = PC+cx16
case (op) // 根據 OP 執行對應的動作
LD: begin // 載入指令: R[ra] = m[addr]
R[ra] = {m[addr], m[addr+1], m[addr+2], m[addr+3]};
$write("%4dns %8x : LD %x,%x,%-4x", $stime, `PC, ra, rb, cx16);
end
ST: begin // 儲存指令: m[addr] = R[ra]
{m[addr], m[addr+1], m[addr+2], m[addr+3]} = R[ra];
$write("%4dns %8x : ST %x,%x,%-4x", $stime, `PC, ra, rb, cx16);
end
ADD: begin // 加法指令: R[ra] = R[rb]+R[rc]
R[ra] = R[rb]+R[rc];
$write("%4dns %8x : ADD %x,%x,%-4x", $stime, `PC, ra, rb, rc);
end
JMP:begin // 跳躍指令: PC = PC + cx24
addr = cx24; // 取出 cx 並轉為 32 位元有號數
`PC = `PC + addr; // 跳躍目標位址=PC+cx
$write("%4dns %8x : JMP %-8x", $stime, `PC, cx24);
end
endcase
$display(" R[%2d]=%4d", ra, R[ra]); // 顯示目標暫存器的值
end
endmodule
module main; // 測試程式開始
reg clock; // 時脈 clock 變數
cpu0mc cpu(clock); // 宣告 cpu0mc 處理器
initial clock = 0; // 一開始 clock 設定為 0
always #10 clock=~clock; // 每隔 10 奈秒將 clock 反相,產生週期為 20 奈秒的時脈
initial #640 $finish; // 在 640 奈秒的時候停止測試。(因為這時的 R[1] 恰好是 1+2+...+10=55 的結果)
endmodule
測試結果
上述程式使用 icarus 測試與執行的結果如下所示。
D:\Dropbox\Public\web\oc\code>iverilog -o cpu0mc cpu0mc.v
D:\Dropbox\Public\web\oc\code>vvp cpu0mc
10ns 00000004 : LD 1,f,0018 R[ 1]= 1
30ns 00000008 : LD 2,f,0010 R[ 2]= 0
50ns 0000000c : LD 3,f,0014 R[ 3]= 0
70ns 00000010 : ADD 2,2,1 R[ 2]= 1
90ns 00000014 : ADD 3,3,2 R[ 3]= 1
110ns 0000000c : JMP fffff4 R[15]= 12
130ns 00000010 : ADD 2,2,1 R[ 2]= 2
150ns 00000014 : ADD 3,3,2 R[ 3]= 3
170ns 0000000c : JMP fffff4 R[15]= 12
190ns 00000010 : ADD 2,2,1 R[ 2]= 3
210ns 00000014 : ADD 3,3,2 R[ 3]= 6
230ns 0000000c : JMP fffff4 R[15]= 12
250ns 00000010 : ADD 2,2,1 R[ 2]= 4
270ns 00000014 : ADD 3,3,2 R[ 3]= 10
290ns 0000000c : JMP fffff4 R[15]= 12
310ns 00000010 : ADD 2,2,1 R[ 2]= 5
330ns 00000014 : ADD 3,3,2 R[ 3]= 15
350ns 0000000c : JMP fffff4 R[15]= 12
370ns 00000010 : ADD 2,2,1 R[ 2]= 6
390ns 00000014 : ADD 3,3,2 R[ 3]= 21
410ns 0000000c : JMP fffff4 R[15]= 12
430ns 00000010 : ADD 2,2,1 R[ 2]= 7
450ns 00000014 : ADD 3,3,2 R[ 3]= 28
470ns 0000000c : JMP fffff4 R[15]= 12
490ns 00000010 : ADD 2,2,1 R[ 2]= 8
510ns 00000014 : ADD 3,3,2 R[ 3]= 36
530ns 0000000c : JMP fffff4 R[15]= 12
550ns 00000010 : ADD 2,2,1 R[ 2]= 9
570ns 00000014 : ADD 3,3,2 R[ 3]= 45
590ns 0000000c : JMP fffff4 R[15]= 12
610ns 00000010 : ADD 2,2,1 R[ 2]= 10
630ns 00000014 : ADD 3,3,2 R[ 3]= 55
從上述輸出訊息當中,您可以看到程式的執行是正確的,其中 R[2] 從 0, 1, 2, ..... 一路上數, 而 R[3] 則從 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55 一路累加上來,完成了我們想要的程式功能。
結語
其實、CPU0mc 這樣的設計應該還不能稱之為快取,而是在程式不大的情況之下,將 SRAM 直接包入在 CPU 當中的一種作法, 這種作法的好處是記憶體存取速度很快,但相對的記憶體成本也很貴,因為這些記憶體是直接用靜態記憶體的方式內建在 CPU 當中的。
這種方式比較像 SOC 系統單晶片的做法,在程式很小的情況之下,直接將記憶體包入 SOC 當中,會得到比較高速的電路, 可惜的是這種做法不像目前的電腦架構一樣,是採用外掛 DRAM 的方式,可以大幅降低記憶體的成本,增大記憶體的容量 就是了。