這次想從頭做一個簡單的 processor,實際把 instruction、register、arithmetic unit、memory 與控制流程一個個串起來。
整個 project 會從最基本的架構開始,邊實作邊驗證,也順便重新熟悉 Verilog、FPGA 與 testbench 的開發流程。
架構介紹
我們先來了解處理器中會包含哪些不同的子元件。理解這些元件後,後續在看 instruction、register、memory 與各種運算邏輯時會比較容易掌握整體關係。
這個處理器主要由幾個子系統組成,包括 control unit、instruction register、program memory、input buffer、data memory、general-purpose register,以及負責算術與邏輯運算的單元。

Control Unit
控制單元(Control Unit)可以視為整個處理器的核心控制中心。
它會從指令暫存器(Instruction Register)讀取目前的 instruction,並根據其中指定的 operation 決定接下來要執行什麼動作。
例如,如果 instruction 要求執行算術運算,control unit 就會控制對應的 arithmetic operation,並指定來源暫存器與目的暫存器。如果需要從外部讀取資料,control unit 則會控制 input buffer,將外部資料送入處理器。
除此之外,control unit 也負責控制 logical operation、data memory 的讀寫,以及 general-purpose register 的操作。
Instruction Register 與 Program Memory
指令暫存器(Instruction Register)是一個 32 位元暫存器,用來保存目前需要執行的 instruction。
一條 instruction 中會包含所要執行的 operation,例如 opcode(操作碼),以及來源資料與目的資料的位置等資訊。也就是說,執行一條 instruction 所需要的資訊,都會包含在 instruction register 中。
多條 instruction 組合起來,就形成一個 Program。Program 是一系列 instruction 的集合,用來讓 processor 完成某項特定工作。這些 instruction 會儲存在程式記憶體(Program Memory)中,processor 再依序將 instruction 讀入 instruction register。
整體流程:
Program Memory
-> Instruction Register
-> Control Unit
-> Execute
-> Next Instruction
Input Buffer、Data Memory 與 GPR
輸入緩衝器(Input Buffer)負責讓 processor 從 FPGA 外部的 GPIO pin 讀取資料。
在這個 processor 的設計中,外部資料不能直接送進一般用途暫存器(General Purpose Register,GPR),而是需要先存入 data memory,之後再搬移到 GPR。
因此輸入資料的流程是:
GPIO
-> Input Buffer
-> Data Memory
-> GPR
輸出資料也是類似的概念。如果要把資料送到外部 GPIO,會先把資料存入 data memory,再由 data memory 將資料送出去。
GPR
-> Data Memory
-> GPIO
Processor 會配置 32 個 GPR,主要用來保存目前正在進行運算的資料。所有 arithmetic operation 與 logical operation 都會以 GPR 中的資料作為輸入,而運算結果也會再次寫回 GPR。
如果之後需要把結果存到 data memory,則透過對應的 instruction 將資料從 GPR 搬到 data memory。
Arithmetic Unit 與 Logical Unit
算術單元(Arithmetic Unit)負責執行 arithmetic operation;
邏輯單元(Logical Unit)則負責 logical operation。
兩者的資料來源都是 GPR,運算完成後,結果也會寫回 GPR。
因此 GPR 是 processor 進行運算時最主要的資料來源與儲存位置,而 data memory 則比較偏向資料暫存,以及 processor 與外部資料之間的交換媒介。
Processor 的基本運作流程
整體來看,一個 program 會由多條 instructions 組成,並儲存在 program memory 中。
processor 每次取出一條 instruction 放入 instruction register,再由 control unit 解析 instruction,控制不同單元完成對應的 operation。
-> Instruction Register
-> Control Unit
-> Arithmetic / Logical / Memory Operation
-> Write Result
-> Next Instruction
而資料本身則主要在 GPR、data memory 與外部 I/O 之間流動。
這些就是目前這個 processor 最基本的組成與資料流程。
指令的結構
首先會碰到的概念是指令本身的組成結構
前面有稍微介紹到 IR 儲存的是目前準備被執行的 instruction,一條 instruction 通常包含:
┌────────┬─────────┬─────────┬─────────────┐
│ Opcode │ Source1 │ Source2 │ Destination │
└────────┴─────────┴─────────┴─────────────┘
例如:
ADD R2, R1, R0
可以拆成:
Opcode = ADD
Source 1 = R1
Source 2 = R0
Destination = R2
也就代表:R2 = R1 + R0,把 R1 和 R0 加起來放到 R2 的意思。
Three-address Instruction
這個就前面的例子。
三個 Address:Source 1、Source 2、Destination。
例如:
ADD R2, R1, R0
等價於:R2 = R1 + R0,特點是 Operand 都明確寫出來。
Two-address Instruction
只有兩個 Address:Destination / Source、Source
例如:
ADD R0, R1
表示 R0 = R0 + R1,也就代表把 R1 和 R0 加起來,再放回 R0 的意思。
Single-address Instruction
只有一個 Address 明確出現在 Instruction。
另一個 Operand 與 Destination 隱含使用:Accumulator,A。
例如:
ADD R0
代表 A = A + R0,如果想完成 R0 = R0 + R1
就需要:
MOV A, R1
ADD R0
MOV R0, A
也就是說,先把 R1 的值搬到 A,接著用 A 和 R0 做加法,最後再把運算結果搬回 R0。
相較於 Three-address 的原本一條指令就完成,現在需要多條。
Zero-address Instruction
Instruction 完全沒有 Operand Address,通常使用 Stack Architecture
概念:
Stack
│ ... │
├─────────┤
│ r1 │ <- Top
├─────────┤
│ r0 │
└─────────┘
ADD 時:
POP operand 1
POP operand 2
ADD
PUSH result
Operand 的位置由 stack 隱含決定,因此 Instruction 本身不需要寫 Register Address。
四種 Instruction 對照
| 類型 | 明確指定的 Operand | ADD 概念 |
|---|---|---|
| Three-address | 2 Source + 1 Destination | add r2, r1, r0 |
| Two-address | Source + Destination | add r0, r1 |
| Single-address | 1 Source | add r0,使用 Accumulator |
| Zero-address | 無 | 使用 Stack |
往下 Instruction Format 較簡單,但通常完成相同工作需要更多 instructions。反過來,Three-address 每條 instruction 需要編碼更多 Operand 資訊,但常能用較少的 Instructions 完成工作。
我們目的是設計 32 個 GPR + Three-address Instruction,因此最典型的 Arithmetic Instruction 會長得像:
ADD Rd, Rs1, Rs2
Rd = Rs1 + Rs2
定址模式 Addressing Modes
在開始討論 Instruction Register 的內容之前,我們還需要先了解設計 Processor 時的第二個基本概念,也就是 Addressing Mode(定址模式)。
Addressing Mode 用來表示一條 Instruction 要如何取得所需要的資料。
我們已經知道 processor 會執行各種 instructions,而 instruction 用什麼方式取得資料,就是 Addressing Mode 所描述的內容。
常見的 Addressing Mode 有四種:
- Register Addressing Mode
- Immediate Addressing Mode
- Direct Addressing Mode
- Indirect Addressing Mode
Register Addressing Mode
在 Register Addressing Mode(暫存器定址)中,Instruction 所需要的資料存放在 Processor 的 General Purpose Register 中。
例如使用 MOV 指令:
MOV R0, R1
其中 R0 是 Destination Register,而 R1 是 Source Register。
這條指令的意思是,把 R1 中的內容複製到 R0。
例如 R1 = 3,執行:
MOV R0, R1
之後 R0 = 3。
因為 Instruction 所需要的資料直接來自 Register,所以稱為 Register Addressing Mode。
Immediate Addressing Mode
在 Immediate Addressing Mode(立即定址)中,Instruction 所需要的資料會直接寫在 Instruction 裡。
也就是說,不需要再去 Register 或 Memory 中尋找資料。
例如:
MOV R0, #10
這裡的 # 用來表示後面的 10 是一個資料值,而不是 Address。
執行之後 R0 = 10,因此,Immediate Addressing Mode 的特點就是資料直接包含在 Instruction 本身。
Direct Addressing Mode
在 Direct Addressing Mode(直接定址)中,Instruction 不直接提供資料,而是直接提供資料所在的 Address。
例如:
MOV R0, @20H
這裡的 20H 代表一個 Address。
假設 Memory[20H] = 71
執行 Instruction 後 R0 = 71,也就是說,Instruction 直接告訴 Processor:「到 Address 20H 取得資料。」
簡單來說就是 Instruction -> Address -> Data,這就是 Direct Addressing Mode。
Indirect Addressing Mode
最後是 Indirect Addressing Mode(間接定址)。
在這種模式中,Instruction 指定的 Register 並不是直接存放我們需要的資料,而是存放資料所在的 Address。
例如:
MOV R0, @R31
假設 R31 = 1,而 Address 1 中存放 Memory[1] = 3,也就是說 R31 中的 1 是一個 Address。
Processor 會先取得 R31 = 1,再到 Memory Address 1 取得真正的資料 Memory[1] = 3
因此執行:
MOV R0, @R31
之後獲得 R0 = 3。
Direct Addressing 和 Indirect Addressing 最大的差異在於:
- Direct Addressing 是直接在 Instruction 中指定資料所在的 Address。
- Indirect Addressing 則是 Instruction 指定一個 Register,而這個 Register 裡面再存放真正的資料 Address。
以上就是四種 Addressing Modes。
認識 Instruction Register
我們接下來開始設計 Instruction Register(指令暫存器)的邏輯。
首先要先了解 Instruction Register 裡有哪些不同的 Field(欄位)。Field 的作用,就是讓我們可以根據特定位元區間,判斷這些 bits 所代表的資料類型。
這裡假設 Processor 使用一個 32-bit Instruction Register,因此 bit 範圍是 [31:0]
整體格式可以先看成:
31 27 26 22 21 17 16 15 0
┌─────────────┬──────────────┬──────────────┬──┬──────────────────┐
│ Operation │ Destination │ Source 1 │M │ Source 2 / Imm │
│ 5 bits │ 5 bits │ 5 bits │ │ │
└─────────────┴──────────────┴──────────────┴──┴──────────────────┘
Operation Type
最前面的 5 bits [31:27] 用來表示要執行的 Operation Type,也就是 Instruction 的操作種類。因為使用 5 bits,所以最多可以表示 32 種不同的操作。
例如這些編碼未來可以分別代表:
ADD
SUB
MOV
AND
OR
當 Control Unit 讀到這 5 bits 時,就可以知道目前這條 Instruction 要執行什麼操作。
Destination Register
接下來的 5 bits [26:22],代表 Destination Register,也就是運算完成後,結果要寫到哪一個 General Purpose Register。
因為這個欄位也是 5 bits,所以可以表示 0 ~ 31,因此 Processor 可以有 32 個 General Purpose Registers:R0 ~ R31。
在程式實作上,可以把這些 Registers 想成一個具有 32 個元素的 Array。例如 Destination = 00101,也就是十進位的 5,代表 R5。
所以這個欄位的值,本質上就是用來當作 Register Array 的 Index。
Source Register 1
接下來的 5 bits [21:17],代表第一個 Source Register,它同樣可以指定 R0 ~ R31。
例如 Source 1 = 00011,就表示第一個 Operand 要從 R3 取得。
因此可以簡單理解成 Source Register 提供輸入資料,Destination Register 儲存運算結果。
例如:
ADD R2, R1, R0
其中:
Destination = R2
Source 1 = R1
Source 2 = R0
Mode Selection
第 16 bit 是 Mode Selection Bit。
它的用途是決定第二個 Operand 要從哪裡取得,因為像 Addition 這類運算通常需要兩個輸入。
第一個 Source 已經由 [21:17] 指定,而第二個 Source 則有兩種可能:Register 或 Immediate Data,因此透過 Mode Selection Bit 來區分。
如果 Mode = 0,表示第二個 Operand 來自 General Purpose Register,這時 [15:11] 會被當作 Source Register 2。
例如當我 Source 2 = 00100,表示第二個 Operand 來自 R4,所以像:
ADD R2, R1, R4
此時 Instruction 使用的是 Register Addressing。
如果 Mode = 1,表示第二個 Operand 不來自 Register,而是直接使用 Instruction 裡面的 Immediate Data。
這時整個 [15:0] 都會被解讀成 Immediate Data。
因此 Immediate Data 最大可以有 16 bits。例如概念上:
ADD R2, R1, #10
就表示 R2 = R1 + 10,這時 10 並不是 Register Address,而是直接包含在 Instruction 裡的資料。
整個 32-bit Instruction Format 可以整理成:
Mode = 0:Register Operand
31 27 26 22 21 17 16 15 11 10 0
┌─────────┬──────────┬──────────┬──┬──────────┬─────────────┐
│ Opcode │ Dest │ Source 1 │0 │ Source 2 │ unused │
│ 5 bits │ 5 bits │ 5 bits │ │ 5 bits │ │
└─────────┴──────────┴──────────┴──┴──────────┴─────────────┘
如果使用 Immediate:
Mode = 1:Immediate Operand
31 27 26 22 21 17 16 15 0
┌─────────┬──────────┬──────────┬──┬─────────────────────────┐
│ Opcode │ Dest │ Source 1 │1 │ Immediate Data │
│ 5 bits │ 5 bits │ 5 bits │ │ 16 bits │
└─────────┴──────────┴──────────┴──┴─────────────────────────┘
後續在實作程式碼時,可以再利用 define 為這些 bit ranges 取名字,避免程式裡一直直接使用 [31:27]、[26:22] 這類數字,讓 Instruction Decode 的程式碼更容易閱讀。
程式碼實做新增 IR 以及 GPR
這裡我是用 Xilinx Vivado,建立一個新的 project 以及 design source,名稱就老套的 top,目前暫時不需要加入 I/O。
首先宣告前面設計好的 32-bit Instruction Register:
reg [31:0] IR;
還記得前面已經將 IR 分成幾個不同的 Field 嗎:
31 27 26 22 21 17 16 15 11 10 0
┌─────────┬──────────┬──────────┬──┬──────────┬─────────────┐
│ Op Type │ RDST │ RSRC1 │M │ RSRC2 │ Unused │
└─────────┴──────────┴──────────┴──┴──────────┴─────────────┘
在 Register Addressing Mode 中:
[31:27] Operation Type
[26:22] Destination Register
[21:17] Source Register 1
[16] Mode Selection
[15:11] Source Register 2
[10:0] Unused
如果使用 Immediate Addressing Mode,前面的欄位相同,但 [15:0] 會全部作為 Immediate Data:
31 27 26 22 21 17 16 15 0
┌─────────┬──────────┬──────────┬──┬─────────────────────────┐
│ Op Type │ RDST │ RSRC1 │1 │ Immediate Data │
└─────────┴──────────┴──────────┴──┴─────────────────────────┘
由於 Source Register 與 Destination Register 都使用 5 bits 表示,因此可以表示的 Register Address 範圍是 00000 ~ 11111(0 ~ 31),因此這個 Processor 需要有 32 個 General Purpose Registers。
Immediate Data 的大小是 16 bits,為了讓 Processor 中的資料寬度保持一致,我們也將每一個 General Purpose Register 設計成 16 bits。
Verilog 可以這樣宣告:
reg [15:0] GPR [31:0];
這裡要分成兩部分理解:
[15:0],代表每一個 Register 都是 16 bits。[31:0],代表 GPR 共有 32 個位元。
所以可以把它理解成:
GPR[0] = R0
GPR[1] = R1
GPR[2] = R2
...
GPR[31] = R31
也就是一個包含 32 個 16-bit Registers 的 Array。
之所以有 32 個 Registers,是因為 Instruction 中使用 5 bits 表示 Register Address:
如果今天 Source 和 Destination Address 改成 6 bits,就可以表示 R0 ~ R63。
這時 GPR Array 就必須跟著改成:
reg [15:0] GPR [63:0];
所以 Instruction Format 中 Register Address Field 的大小,會直接決定 Processor 可以擁有多少個 General Purpose Registers。
接下來還有一個重要工作,就是替 IR 中不同的 Field 取名字。
如果每次都直接寫:
IR[31:27] IR[26:22] IR[21:17] IR[16] IR[15:11] IR[15:0]
程式碼會比較難閱讀,因此可以使用 Verilog 的 `define Directive 為這些 Field 定義名稱。
例如 Operation Type 位於 IR[31:27] 可以定義成:
`define OP_TYPE IR[31:27]
之後在程式中看到 `OP_TYPE,Preprocessor 就會將它展開成 IR[31:27]
同樣地,可以將其他 Field 定義成:
`define OP_TYPE IR[31:27] `define RDST IR[26:22] `define RSRC1 IR[21:17] `define IMM_MODE IR[16] `define RSRC2 IR[15:11] `define IMM_DATA IR[15:0]
這樣程式碼的意義會比直接操作 Bit Range 清楚很多。
算術指令(Arithmetic Instructions)
接下來要開始加入 Processor 所支援的 Arithmetic Instructions(算術指令)。
這一段會加入幾種基本指令,包括兩種 MOV,以及三種算術運算:
MOV
MOVSGPR
ADD
SUB
MUL
其中 SGPR 是一個 Special General Purpose Register,主要用在乘法運算。後面會看到為什麼需要額外設計一個專門操作 SGPR 的 MOV 指令。
一般 MOV Instruction
一般的 MOV 用來把資料複製到某個 General Purpose Register。
例如:
MOV Rdest, Rsrc
意思就是 Rdest = Rsrc,也就是將 Source Register 的內容複製到 Destination Register。
不過這個 MOV 同時支援前面介紹過的兩種 Addressing Mode。
如果使用 Register Addressing Mode(Mode = 0),那麼 Rdest = Rsrc。例如:
MOV R2, R1
就是把 R1 的內容複製到 R2。
如果使用 Immediate Addressing Mode(Mode = 1),則 Source 不再是某個 Register,而是 Instruction 中直接提供的 Immediate Data,Rdest = Immediate Data。例如概念上:
MOV R2, #10
就是 R2 = 10。
MOV SGPR Instruction
除了普通的 MOV 之外,Processor 還會另外提供一個 MOVSGPR。這個 Instruction 專門用來讀取 Special General Purpose Register,SGPR 的內容。

一般 MOV 處理的是 GPR -> GPR‘、Immediate Data -> GPR,而 MOV SGPR 則是 SGPR → GPR。之所以需要這個特殊 Register,主要是因為 Multiplication 操作。
Arithmetic Instructions
Processor 目前會支援三種 Arithmetic Operations:
ADD
SUB
MUL
這三種指令都支援 Register 和 Immediate 這兩種 Addressing Mode。
以 ADD 為例,基本形式可以理解成 Rdest = Rsrc1 + Source2,其中第一個 Source 一定來自 Rsrc1,但第二個 Source 要看 IR[16]。
如果 Mode = 0,第二個 Source 來自 General Purpose Register,Rdest = Rsrc1 + Rsrc2。同樣的概念也適用於 SUB 與 MUL。
因此:
SUB Rdest, Rsrc1, Source2
可以是 Rdest = Rsrc1 - Rsrc2 也可以是 Rdest = Rsrc1 - Immediate
MUL 也是相同:Rdest = Rsrc1 × Rsrc2 或 Rdest = Rsrc1 × Immediate
Arithmetic Unit
目前 Processor 中共有 32 個 General Purpose Registers,每一個 Register 的大小都是 16 bits,這些 Registers 會和 Arithmetic Unit 連接。
Arithmetic Unit 負責 ADD、SUB、MUL 等算術運算。
基本資料流程可以理解成:Arithmetic Unit 從 GPR 取得 Operand,依照 Instruction 指定的 Operation 執行運算,最後再把結果寫回 Destination Register。
為什麼需要 SGPR
ADD 與 SUB 的結果通常可以直接存回 16-bit GPR,但 MUL 有一個特別的問題。
我們目前每個 General Purpose Register 都是 16 bits,如果將兩個 16-bit 數值相乘,完整結果最多需要 32 bits,但一個普通 GPR 只能保存 16 bits,因此完整的乘法結果無法全部存入一個 GPR。
所以這個 Processor 額外加入 SGPR,也就是 Special General Purpose Register。
當執行乘法之後,這 32 bits 會被拆成兩部分:
31 16 15 0
┌───────────────────────┬────────────────────────┐
│ Upper 16 bits │ Lower 16 bits │
│ MSB │ LSB │
└───────────────────────┴────────────────────────┘
其中較低的 16 bits,也就是 Result[15:0],會寫入使用者指定的 Destination Register。
例如:
MUL R2, R1, R0
乘法完成後 R2 = Result[15:0],而較高的 16 bits Result[31:16] 則會保存到 SGPR,也就是 SGPR = Result[31:16]。
例如:
32-bit multiplication result
┌───────────────────────┬────────────────────────┐
│ Result[31:16] │ Result[15:0] │
└───────────┬───────────┴────────────┬───────────┘
│ │
▼ ▼
SGPR Rdest
如果使用者只需要乘法結果較低的 16 bits,就直接使用 Destination Register 即可。
但如果需要取得較高的 16 bits,就必須透過 MOV 把 SGPR 的內容搬到一般的 General Purpose Register。因此 MOV SGPR 並不是另一種一般的資料搬移方式,而是專門用來讀回乘法產生的 Upper 16 bits。
所以目前的 Processor 架構除了原本的 32 個 16-bit General Purpose Registers 之外,還會再加入一個專門保存乘法高位元結果的 SGPR。
實做 Operation Code
接下來要為前面定義好的 Arithmetic Instructions 配置對應的 Operation Code,並開始實作實際的運算邏輯。
前面已經知道,Instruction Register 的 IR[31:27],共有 5 bits,用來表示 Operation Type,最多可以定義 32 種不同的 operation。每一種 instruction 都必須有自己唯一的 code,processor 才能知道目前要執行哪一種操作。
可以利用 `define 配置不同的 Operation Code:
`define MOVSGPR 5'b00000 `define MOV 5'b00001 `define ADD 5'b00010 `define SUB 5'b00011 `define MUL 5'b00100
也就是:
MOVSGPR -> 0
MOV -> 1
ADD -> 2
SUB -> 3
MUL -> 4
Control Logic 會根據 OP_TYPE 的值判斷 Instruction 的種類。之後如果增加新的 Instruction,就可以繼續配置新的 Code。
前面提過,兩個 16-bit 數值相乘時,完整結果最多需要 32 bits,乘法結果會拆成:
Lower 16 bits -> Destination GPR
Upper 16 bits -> SGPR
因此 SGPR 本身只需要 16 bits:
reg [15:0] SGPR;
因為乘法會產生最多 32-bit 的結果,所以還需要一個暫時保存完整結果的變數:
reg [31:0] mul_result;
mul_result 只是暫時保存完整乘法結果,之後再分別把高 16 bits 與低 16 bits放到對應的位置。
實作 Instruction Logic
接下來開始實作 MOV SGPR、MOV、ADD、SUB 和 MUL。
這裡使用:
always @(*)
表示:
只要這個 block 所使用到的任何輸入值發生變化,就重新計算裡面的邏輯。
因此,當 IR 改變時,對應的 Operation、Source、Destination 或 Immediate Data 也會跟著改變,這段邏輯就會重新被執行。
接著使用 case 判斷目前的 Operation:
always @(*) begin
case (`OP_TYPE)
// 待會實做
endcase
end
意義上就是根據 OP_TYPE(IR[31:27]),判斷目前 Instruction 是 MOV、ADD、SUB、MUL 或其他 Operation。
MOV SGPR
如果 OP_TYPE = MOV_SGPR,代表需要把 SGPR 的內容搬到某一個普通的 GPR。
程式可以寫成:
`MOV_SGPR:
GPR[`RDST] = SGPR;
例如假設 RDST = 3,那也就是 SGPR -> R3,這樣就可以把先前乘法保存在 SGPR 裡的 Upper 16 bits 取出來,放到普通的 General Purpose Register 中。
MOV
程式概念是:GPR[RDST] = IMM_DATA; 或是 GPR[RDST] = GPR[RSRC1];,所以完整的 MOV 邏輯概念上可以寫成:
`MOV: begin
if (`IMM_MODE)
GPR[`RDST] = `IMM_DATA;
else
GPR[`RDST] = GPR[`RSRC1];
end
也就是:
IMM_MODE = 1
-> Immediate Data -> Rdest
IMM_MODE = 0
-> Rsrc1 -> Rdest
同一個 MOV Instruction,可以根據 IR[16] 決定資料到底來自 Register 還是直接來自 Instruction 裡的 Immediate Data。
ADD
ADD 的基本形式是 Rdest = Rsrc1 + Source2 或 Rdest = Rsrc1 + Immediate Data,依據 mode 決定。
程式可以寫成:
`ADD: begin
if (`IMM_MODE)
GPR[`RDST] = GPR[`RSRC1] + `IMM_DATA;
else
GPR[`RDST] = GPR[`RSRC1] + GPR[`RSRC2];
end
SUB
SUB 的邏輯與 ADD 相同,只是將 Addition 改成 Subtraction。
可以寫成:
`SUB: begin
if (`IMM_MODE)
GPR[`RDST] = GPR[`RSRC1] - `IMM_DATA;
else
GPR[`RDST] = GPR[`RSRC1] - GPR[`RSRC2];
end
MUL
與 ADD、SUB 不同的是,兩個 16-bit 數值相乘後,完整結果最多可能需要 32 bits。
因此前面已經準備了一個 32-bit Temporary Register:
reg [31:0] mul_result;
先將完整的 Multiplication Result 存進 mul_result,如果 IMM_MODE = 1,則
mul_result = Rsrc1 * Immediate Data
如果 IMM_MODE = 0,則:
mul_result = Rsrc1 * Rsrc2
Verilog 可以寫成:
`MUL: begin
if (`IMM_MODE)
mul_result = GPR[`RSRC1] * `IMM_DATA;
else
mul_result = GPR[`RSRC1] * GPR[`RSRC2];
GPR[`RDST] = mul_result[15:0];
SGPR = mul_result[31:16];
end
測試
接下來要為目前已經完成的 Arithmetic Instructions 加上 testbench,確認每一個 Instruction 是否按照預期運作。
與其等整個 Processor 完成後才一次驗證所有功能,比較好的方式是每完成一個小功能就立即測試。這樣如果結果有問題,可以直接針對剛加入的功能檢查,而不需要最後再從整份 Processor Code 中尋找錯誤。
目前要測試的主要功能包括 ADD 和 MOV,而且兩者都要分別驗證兩種 Addressing Mode。
建立 Testbench
首先在 Vivado 的 simulation Sources 中新增 testbench。建立 testbench 後,還需要處理前面在 RTL 中宣告的 define。主要是因為 testbench 也希望使用這些名稱,而不是直接操作 IR[31:27]、IR[26:22] 等 bit range。
因此在 Vivado 中,可以將 top 設為 “Set Global Include”,這樣其中的 `define 就可以在 testbench 中使用。
建立 DUT
Testbench 中需要實例化我們的 RTL,instance 名稱可以使用 dut,也就是 Design Under Test。
因為目前 top 還沒有 Input 或 Output Port,所以可以直接寫:
top dut();
之後 testbench 就可以透過 hierarchical reference 存取 top 裡面的資料,例如:
dut.IR dut.GPR[0] dut.GPR[1]
初始化 GPR
Processor 中目前有 32 個 GPR,而在測試之前,希望先把這些 registers 設定成已知值,這樣比較容易判斷運算結果是否正確。
因此可以先宣告:
integer i;
再使用 for loop:
for (i = 0; i < 32; i = i + 1) begin
dut.GPR[i] = 16'd2;
end
這樣所有 GPR 一開始都有相同的已知值 2。
測試 ADD Immediate
第一個要測試的是「ADD with Immediate Addressing」。
首先將 Instruction Register 清為:
dut.IR = 32'd0;
因為這次第二個 Operand 使用 Immediate Data,所以 IMM_MODE = 1。假設:
Source 1 = R2
Destination = R0
Immediate = 4
概念上的 Instruction 就是:
ADD R0, R2, #4
因為前面已經初始化 R2 = 2,所以運算結果應該是:
R0 = R2 + 4
= 2 + 4
= 6
Testbench 可以設定:
dut.`IMM_MODE = 1'b1; dut.`OP_TYPE = `ADD; dut.`RSRC1 = 5'd2; dut.`RDST = 5'd0; dut.`IMM_DATA = 16'd4;
最後可以使用 $display 印出結果:
$display("ADD Immediate: SRC1=%d IMM=%d RESULT=%d",
dut.GPR[2],
dut.`IMM_DATA,
dut.GPR[0]);
預期:
R2 = 2
Immediate = 4
2 + 4 -> R0
R0 = 6
測試 ADD Register Mode
接下來測試「ADD with Register Addressing」。這次 IMM_MODE = 0,代表兩個 Operand 都來自 GPR。
例如:
Source 1 = R4
Source 2 = R5
Destination = R0
也就是:
ADD R0, R4, R5
由於初始化時 R4 = 2 和 R5 = 2,所以:
R0 = R4 + R5
= 2 + 2
= 4
Testbench 可以設定:
dut.IR = 32'd0; dut.`IMM_MODE = 1'b0; dut.`OP_TYPE = `ADD; dut.`RSRC1 = 5'd4; dut.`RSRC2 = 5'd5; dut.`RDST = 5'd0;
並印出:
$display("ADD Register: SRC1=%d SRC2=%d RESULT=%d",
dut.GPR[4],
dut.GPR[5],
dut.GPR[0]);
因此預期:
R4 = 2
R5 = 2
2 + 2 -> R0
R0 = 4
測試 MOV Immediate
首先使用 Immediate Addressing:
MOV R4, #55
也就是:
55 -> R4
因此:
IMM_MODE = 1
OP_TYPE = MOV
RDST = R4
IMM_DATA = 55
Testbench 可以設定:
dut.IR = 32'd0; dut.`IMM_MODE = 1'b1; dut.`OP_TYPE = `MOV; dut.`RDST = 5'd4; dut.`IMM_DATA = 16'd55;
執行之後預期 R4 = 55,可以使用以下確認結果:
$display("MOV Immediate: IMM=%d DEST=%d",
dut.`IMM_DATA,
dut.GPR[4]);
測試 MOV Register Mode
接下來測試 Register Addressing:
MOV R4, R7
代表 R7 -> R4。這次:
IMM_MODE = 0
OP_TYPE = MOV
RDST = R4
RSRC1 = R7
Testbench 可以設定:
dut.IR = 32'd0; dut.`IMM_MODE = 1'b0; dut.`OP_TYPE = `MOV; dut.`RDST = 5'd4; dut.`RSRC1 = 5'd7;
執行之後預期 R4 = R7。如果 R7 原本是 2,那麼 R4 = 2。可以透過:
$display("MOV Register: SRC=%d DEST=%d",
dut.GPR[7],
dut.GPR[4]);
確認 source 與 destination 的值。
目前測試的資料流可以整理成:
ADD Immediate
R2 + Immediate
2 + 4
|
v
6 -> R0
ADD Register
R4 + R5
2 + 2
|
v
4 -> R0
MOV Immediate
55 -> R4
MOV Register
R7 -> R4
這裡也再次看到前面使用 `define 的好處。完成 testbench 後,就可以執行 Run Behavioral Simulation,確認目前加入的 Arithmetic Instructions 是否能得到預期結果。
這種做法的核心是每加入一組 Processor 功能就立即驗證,例如先完成 Arithmetic Unit 就測 Arithmetic Instructions,後續再加入新的功能並分別測試,而不是等整個 Processor 完成後才一次 Debug。
目前還沒有執行 multiplication,因此和乘法相關的 SGPR、mul_result 都還沒有被寫入有效資料,所以在 simulation 中會看到 undefined value 是正常的,因為它們目前尚未初始化,也還沒有經過 MUL instruction 更新。

測試 multiplication
接下來可以在 testbench 中加入 multiplication 的測試。
要求是 GPR0 * GPR1 -> 32-bit result,其中:
Lower 16 bits -> GPR2
Upper 16 bits -> GPR3
不過依照目前 processor 的設計,MUL instruction 本身會先將結果拆成:
Lower 16 bits -> GPR2
Upper 16 bits -> SGPR
因此還需要再執行一次 MOVSGPR,把 SGPR 的內容搬到 GPR3。
可以先設定兩個較大的數值,確保 multiplication result 真的超過 16 bits:
dut.GPR[0] = 16'hFFFF; dut.GPR[1] = 16'h0002;
接著執行 multiplication:
dut.IR = 32'd0; dut.`IMM_MODE = 1'b0; dut.`OP_TYPE = `MUL; dut.`RSRC1 = 5'd0; dut.`RSRC2 = 5'd1; dut.`RDST = 5'd2; #10;
這時:
GPR0 * GPR1
-> Lower 16 bits 存入 GPR2
-> Upper 16 bits 存入 SGPR
接著使用 MOVSGPR:
dut.IR = 32'd0; dut.`OP_TYPE = `MOVSGPR; dut.`RDST = 5'd3; #10;
將 SGPR -> GPR3,最後可以使用 $display 查看結果:
$display("MUL Register");
$display("Source 1 = %h", dut.GPR[0]);
$display("Source 2 = %h", dut.GPR[1]);
$display("LSB = %h", dut.GPR[2]);
$display("MSB = %h", dut.GPR[3]);
因此完整流程可以理解成:
GPR0 * GPR1
|
v
32-bit result
|
+-> [15:0] -> GPR2
|
+-> [31:16] -> SGPR -> MOVSGPR -> GPR3
這個 testbench 不只是在確認 MUL 是否正常,也同時驗證了 SGPR 與 MOVSGPR 的功能。
後記
原本以為這篇應該不會太長,結果寫一寫還是超標不少(?)
不過這次的收穫比預期多非常多,一方面是已經實做過 CHIP-8 emulator,所以進入狀況的速度明顯快了不少;另一方面,這也算是睽違三年再次回到需要自己動手操作硬體的領域,很多工具和操作方式都得重新適應。
另外,也可能是工作的關係,現在的吸收狀態也比當初硬把知識點吃下的我還要好太多了,包含對於 testbench 的理解、top module 寫法,以及整個設計和驗證流程等等,都比以前更容易抓到重點。
要是我工作初期就實做這個迷你 project,不知道會不會更快進入狀況一點?畢竟三年過去了,莫名其妙成為了自幹了一套 EDA tool 的人(可見我私底下花了多少時間惡補硬體背景知識),現在再回頭看這些東西,反而會覺得,嗯,好像很多地方都還可以再設計得更好一點。
總而言之,大概就是相輔相成吧XD
雖然多少犧牲了一點生活和健康,但比起數學那個有些飄渺的世界,至少也真正體會到了現實世界裡這些東西的有趣。未來或許還是會想辦法再往數學靠攏一些。
References
- 封面圖由 DIvan Chumak 拍攝





