為什麼要先認識暫存器#
在學習 RISC-V 指令集之前,我們需要先建立對暫存器的基本認識:每個暫存器的特殊用途,以及呼叫函式前後該如何保存各個暫存器的內容。這兩件事其實就是 RISC-V 的呼叫慣例(Calling Convention),這些規範在編譯器的實作上都看得到,理解後對嵌入式系統軟體開發很有幫助。
本章前半部聚焦在通用暫存器、Caller save 與 Callee save 的觀念;後半部則會逐一走過 RV32I 指令集中的各類指令。
通用暫存器#
下圖列舉了 RISC-V 處理器中的通用暫存器:

針對上圖,補充以下幾個重點:
x0 暫存器#
x0 暫存器的數值為 0 且無法更動,我們可以利用它做到類似 mov 的作用:
add a0, a1, x0 # a0 = a1Return Address(ra)#
Return Address 存放函式執行結束時返回的位置。假設有兩個函式 a、b:
main() -> a()
a() -> b()因為 main() 呼叫了函式 a(),在跳進函式 a() 執行前,main() 會將 ra 暫存器的值放到 Stack 上。此時,如果函式 a() 又呼叫了函式 b(),在呼叫之前我們會將函式 a() 的 return address 儲存到 Stack,避免 ra 暫存器被函式 b() 複寫後無法返回到 main() 繼續執行。由此可知,ra 由呼叫者維護暫存器的狀態,這類特性的暫存器都是 Caller save。
在這邊如果不考慮 Stack pointer,Stack 內部看起來會是:
+------------------------------+
| Data stored by function b |
+------------------------------+
| Return Address of function a |
+------------------------------+
| Return Address of main |
--+------------------------------+--Stack pointer(sp)#
每個 Process 都會有屬於自己的區域變數,這些變數同樣會被存放在 Stack 中,Stack pointer 會指向 Stack 中屬於此 Process 的位址。不過,這部分與 ra 會有些不同,sp 暫存器被定義為 Callee save:當被呼叫的函式修改 sp 暫存器的內容之前,必須先將內容保存下來,並在函式返回前恢復原本的值。
如果以 main() 呼叫 function a() 來看,流程如下:
main() 將當前 ra 的值放到 stack -> 進入 function a() -> 將 sp 的值存放到 stack -> 移動 sp 的位址 (Scope of function a) -> 一些運算 -> 返回到 main 之前恢復 sp 的值 -> 跳回 main() -> 恢復 main() 的 raFunction args:a0–a7(x10–x17)#
a0–a7 共 8 個暫存器可供我們用於存取函式的參數值。當我們呼叫函式時,函式的參數會依序的存放到這些暫存器中。
Return values:a0–a1#
a0、a1 暫存器除了會被當作 args 使用外,當函式需要有 return value 時,我們可以將 return value 存放至 a0 與 a1 暫存器,供其他函式取得返回值。
RV32I 指令集總覽#
RV32I 是 32-bit 的基本整數指令集,會使用到 32 個暫存器(x0–x31),且一共有 47 道指令。RV32I 的指令可分成六大類:
| 指令類型 | 說明 |
|---|---|
| I-Type | 包含了暫存器與立即數的運算 |
| R-Type | 此類型的指令會有 3 個暫存器作為 Input,分別是 rd、rs1、rs2 |
| S-Type | 包含存取記憶體的指令 |
| SB-Type | 分支指令(條件跳轉) |
| U-Type | 將立即數放到高位,這些指令被設計來實現完整的 32 bits 運算 |
| UJ-Type | 跳轉指令 |
為了方便,RISC-V 將一個 WORD 的大小設成 4 Bytes(32 bits),同時,RISC-V 的指令長度也是 32 bits,這些空間會被分割成好幾個 fields,不同類型的指令都會有不同的分配方式。
I-Format#
這種類型的指令代表暫存器與常數之間的運算:
| 31 20 | 19 15 | 14 12 | 11 07 | 06 00 |
+----------------------+---------+------------+--------+------------+
| immediate[11:0] | rs1 | funct3 | rd | opcode |
+----------------------+---------+------------+--------+------------+ADDI#
addi rd, rs1, simm12常數部分(simm12)為 12 位元的有號數,運算時會將 12 位元擴展成 32 位元,與 rs1 暫存器做加法運算並將結果寫進 rd 暫存器中。
關於 simm12 擴展的邏輯,可以參考 rv32emu-next ↗ 的實作:
// FI_IMM_11_0 = 0b11111111111100000000000000000000 static inline int32_t dec_itype_imm(uint32_t inst){ return ((int32_t)(inst & FI_IMM_11_0)) >> 20; }
SLTI[U]#
slti rd, rs1, simm12運算時會將 12 位元擴展成 32 位元,再與 rs1 暫存器當做 signed number 做比較(如果指令為 SLTIU 則視為無號數)。若 rs1 暫存器小於常數,則將數值 1 寫入 rd 暫存器,否則為 0。
補充 本篇文章的 simm[number] 代表有號的 number 個位元,若是 uimm[number] 則代表無號的 number 個位元。若出現在下方指令,就不再特別說明。
ANDI / ORI / XORI#
andi/ori/xori rd, rs1, simm12將 simm12 擴展後與 rs1 暫存器做 AND / OR / XOR 運算,再將結果寫入 rd 暫存器中。
SLLI / SRLI / SRAI#
slli/srli/srai rd, rs1, uimm5常數部分為 unsigned 5 bits,將 rs1 暫存器做 shift 運算,偏移量為 uimm5 的值(0–31),偏移後的結果會寫入 rd 暫存器。
- SLLI 代表邏輯左移
- SRLI 為邏輯右移
- SRAI 為算數右移
LW / LH / LHU / LB / LBU#
lw/lh/lhu/lb/lbu rd, rs1, simm12將 rs1 暫存器的內容加上 simm12 視為位址,並將該位址存放的值放到 rd 中。
- LW 為載入 32 bits(WORD)資料寫入 rd 中。
- LH/LHU 為載入 16 bits(Half of WORD)資料分別做有號與無號擴展成 32 bits 後寫入 rd 中。
- LB/LBU 為載入 8 bits(BYTE)資料分別做有號與無號擴展成 32 bits 後寫入 rd 中。
U-Format#
包含 upper immediates 的指令。
LUI(Load upper immediate)#
lui rd, uimm20將 uimm20 存放到 rd 暫存器的高位 20 bits 中,剩餘的 12 bits 皆為 0。
AUIPC(add upper immediate to pc)#
auipc rd, uimm20unsigned 20-bit 放到最高 20 位元,剩餘 12 位元補 0,將此數值與 pc 相加寫入 rd 暫存器。
R-Format#
指令為暫存器與暫存器之間的運算。
ADD / SUB#
add/sub rd, rs1, rs2將 rs1 與 rs2 做加法 / 減法運算後,再將結果寫入 rd 暫存器。
SLT / SLTU#
slt/sltu rd, rs1, rs2將 rs1 與 rs2 暫存器當做有號或是無號數做比較,若 rs1 小於 rs2,將數值 1 寫入 rd,反之寫入 0。
AND / OR / XOR#
and/or/xor rd, rs1, rs2將 rs1 與 rs2 做 AND / OR / XOR 運算,並將結果寫入 rd 中。
SLL / SRL / SRA#
sll/srl/sra rd, rs1, rs2將 rs1 做 shift 運算,再將結果寫入 rd 暫存器,rs2 的最低 5 位為代表位移量。
NOP 指令#
NOP 指令即為不改變任何暫存器狀態,除了 pc 以外。NOP 指令會被編碼成 addi x0, x0, 0 替代。
UJ-Format#
UJ-Format 包含了無條件跳躍(Unconditional Jumps)類型的指令。
JAL(jump and link)#
jal rd, simm21常數部分為 21 位元的有號數(此常數必須為 2 的倍數,代表 LSB 必為 0)。因為此道指令編碼的常數位元數只有 20 位元,所以只會將 simm21 的最高 20 位元放入指令編碼中,跳躍範圍為 ±1 MiB。同時也會將下一道指令的位址 pc+4 寫入 rd 暫存器中。在標準的 calling convention 中,rd 暫存器會使用 x1。如果只是單純的 jump,並非是呼叫函式需要儲存其返回位址 pc+4,可用 jal x0, simm21 取代。
JALR(jump and link register)#
jalr rd, rs1, simm12跳躍的位址為 rs 暫存器加上 simm12,並把下一道指令的位址 pc+4 寫入 rd 暫存器中。
SB-Format#
SB-Format 包含了條件跳躍(Conditional Branches)類型的指令。
BEQ / BNE / BLT / BLTU / BGE / BGEU#
beq/bne/blt/bltu/bge/bgeu rs1, rs2, simm13常數部分必須為 2 的倍數,即最低位元為 0。因為此道指令編碼的常數位元數只有 12 位元,所以只會將 simm13 的最高 12 位元放入指令編碼中,跳躍範圍為 ±4 KiB。
- BEQ/BNE:將 rs1 與 rs2 做相同與不同的比較,若成立則跳躍。
- BLT/BLTU:將 rs1 與 rs2 分別做有號 / 無號的小於比較,若成立則跳躍。
- BGE/BGEU:將 rs1 與 rs2 分別做有號 / 無號的大於等於比較,若成立則跳躍。跳躍的位址則為 pc 加上 simm13。
S-Format#
將內容存放到記憶體中的指令。
SW / SH / SB#
sw/sh/sb rs2, rs1, simm12將 rs1 暫存器的內容加上 simm12 視為位址,並將 rs2 的資料寫到該位址中。
- SW 將 rs2 暫存器完整 32 bits 資料寫入位址。
- SH 將 rs2 暫存器最低 16 bits 資料寫入位址。
- SB 將 rs2 暫存器最低 8 bits 資料寫入位址。
Memory model#
定義了一組 FENCE 指令,達到多個 thread 間的記憶體同步。
控制與狀態暫存器指令(Control and Status Register Instructions)#
CSR Instructions#
CSRRW / CSRRS / CSRRC / CSRRWI / CSRRSI / CSRRCI定義了一組 CSR (Control and Status Register) 指令,可用來讀取寫入 CSR。
Timers and Counters#
RDCYCLE[H]#
rdcycle 用來讀取最低 31-bit cycle CSR,rdcycleh 用來讀取最高 31-bit cycle 數。
RDTIME[H]#
用來讀取 time CSR。
RDINSTRET#
用來讀取 instret CSR。
Environment Call and Breakpoints#
ECALL#
屬於 I-Type。
使用來呼叫 system call。
EBREAK#
Debugger 用來切換進 Debugging 環境。