参考

技术规范 Specifications

旧版规范 (20191213, 汪辰老师所用版本)

RISC-V Technical Specifications Archive

参考手册

The RISC-V Reader: An Open Architecture Atlas Authored by David Patterson, Andrew Waterman Edition: 1st

一个可以将 C/C++/Python 的代码转换成 Riscv 汇编的工具网站
Compiler Explorer

汇编程序基本构成

一个完整的 RISC-V 汇编程序有多条 “语句” (statement) 组成, 一条典型的 RISC-V 汇编 “语句” 由3部分组成:

1
[label:] [operation] [comment]

其中方括号表示可选项 (支持空行).

label: GNU 汇编中, 任何以冒号结尾的标识符都被认为是一个标号. label 可以理解为给地址起了一个别名, 方便后面的代码通过 label 引用该地址.

  • 文件标签在程序文件中是全局可见的, 所以定义不可重复. 文本标签通常被作为分支或跳转指令的目标地址, 如:

    1
    2
    3
    loop:      # 定义一个loop标签
    ...
    j loop # 跳转到loop标签处
  • 数字标签属于一种局部标签, 可重复定义, 通常用 0~9 之间的数字定义. 在被引用时, 数字标签通常需要带上字母 “f” 或 “b” 字母后缀, 表示 forward 向前搜索和 backward 向后搜索

    1
    2
    3
    4
    5
        j 1f # 向前寻找并跳转至第一个数字为 1 的标签处
    ...
    1: # 数字标签1
    ...
    j 1b # 向后寻找并跳转至第一个数字为 1 的标签处

operation: operation 有以下几种类型

  • instruction (指令): 直接对应二进制机器指令的"真实"指令

  • pseudo-instruction (伪指令): 为了提高编写代码的效率, 通过伪指令来封装一条或者多条 instruction 来实现一些功能

  • directive (指示/伪操作): 通过类似指令的形式 (以 “.” 开头), 通知汇编器如何控制代码的产生等, 不对应具体的指令, 是写给汇编器看的指令.

  • macro (宏): 采用 .macro /.endm 自定义的宏

    1
    2
    3
    .macro LOAD_IMM reg, imm
    li \reg, \imm
    .endm

    使用宏相比于函数有自己独特的优势. 例如函数调用会导致在函数体内 ra (return address) 寄存器, sp (stack point) 寄存器, 以及 a0 等参数寄存器的值发生变化, 这样不能访问到函数调用前寄存器的原始状态. 而宏不会, 宏只是单纯的文本替换, 不涉及到寄存器的变换.

comment: 汇编语言中采用 #, ; 等方式进行行注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.macro do_nothing   # directive
nop # pseudo-instruction
nop # pseudo-instruction
.endm # directive

.text # directive
.global _start # directive
_start: # Label
li x6, 5 # pseudo-instruction
li x7, 4 # pseudo-instruction
add x5, x6, x7 # instruction
do_nothing # Calling macro
stop: j stop # statement in one line

.end # End of file

Vscode 中 RISC-V Support 插件支持 RISC-V 语法的高亮显示

汇编文件的后缀 .s 和 .S 有大小写的区分. 其中小写 s 后缀表示纯的汇编语言, 不包含头文件和宏等预处理指令; 大写的 S 后缀表示包含头文件和宏等预处理指令的汇编文件.

寄存器

  • RV32I 通用寄存器组包含 32 个通用寄存器 x0 ~ x31 和 1 个 pc 寄存器.
  • 在 RISC-V 中, Hart在执行算术逻辑运算时所操作的数据必须直接来自寄存器.
  • Hart 可以执行在寄存器和内存之间的数据读写操作, 读写操作使用字节 (Byte) 为基本单位进行寻址;
  • 32 位的寄存器可以访问最多 2^32 个字节的内存空间, 2^32 bytes = 4 x (2^10)^3 bytes = 4 G bytes.

大端序与小端序

字节序 (Endianness), 又称端序或尾序, 指多字节数据在内存中的字节存放顺序

大端序 (Big-Endian): 数据的高位字节存放在内存的低地址 (先存高位字节)
小端序 (Little-Endian): 数据的低位字节存放在内存的低地址 (先存低位字节)

1
2
3
4
5
6
7
8
decimal: 287454020
binary: 00010001 00100010 00110011 01000100
hexadecimal: 11 22 33 44

low ----- high
| A| A+1| A+2| A+3|
|0x11|0x22|0x33|0x44| big endian
|0x44|0x33|0x22|0x11| little endian (default)

常用汇编指令

算术运算指令

Instruction Name FMT Description ©
add rd, rs1, rs2 ADD R rd = rs1 + rs2
sub rd, rs1, rs2 SUB R rd = rs1 - rs2
addi rd, rs1, imm ADD Immediate I rd = rs1 + imm
lui rd, imm Load Upper Imm U rd = imm << 12
auipc rd, imm ADD Upper Imm to PC U rd = PC + (imm << 12)
pseudoinstruction Base Instruction(s) Meaning Description ©
nop addi x0, x0, 0 No operation
neg rd, rs sub rd, x0, rs Two’s complement rd = -rs

位运算指令

Instruction Name FMT Description ©
xor rd, rs1, rs2 XOR R rd = rs1 ^ rs2
or rd, rs1, rs2 OR R rd = rs1 | rs2
and rd, rs1, rs2 AND Immediate R rd = rs1 & rs2
xori rd, rs1, imm XOR Immediate I rd = rs1 ^ imm
ori rd, rs1, imm OR Immediate I rd = rs1 | imm
andi rd, rs1, imm AND Immediate I rd = rs1 & imm
pseudoinstruction Base Instruction(s) Meaning Description ©
not rd, rs xori rd, rs, -1 negation rd = ~rs

位移运算指令

Instruction Name FMT Description ©
sll rd, rs1, rs2 Shift Left Logical R rd = rs1 << rs2
srl rd, rs1, rs2 Shift Right Logical R rd = rs1 >> rs2
slli rd, rs1, imm Shift Left Logical Imm I rd = rs1 << imm[0:4]
srli rd, rs1, imm Shift Right Logical Imm I rd = rs1 >> imm[0:4]
sra rd, rs1, rs2 Shift Right Arithmetic R rd = rs1 >> rs2
srai rd, rs1, imm Shift Right Arithmetic Imm I rd = rs1 >> imm[0:4]
Instruction Name FMT Description ©
slt Set Less Than R rd = (rs1 < rs2) ? 1 : 0
sltu Set Less Than (U) R rd = (rs1 < rs2) ? 1 : 0
slti Set Less Than Imm I rd = (rs1 < imm) ? 1 : 0
sltiu Set Less Than Imm (U) I rd = (rs1 < imm) ? 1 : 0

U 表示 Usigned Extended

内存读写指令

Instruction Name FMT Description ©
lb rd, imm(rs1) Load Byte (Signed Extended) I rd = M[rs1+imm] [0:7]
lh rd, imm(rs1) Load Half (Signed Extended) I rd = M[rs1+imm] [0:15]
lw rd, imm(rs1) Load Word I rd = M[rs1+imm] [0:31]
lbu rd, imm(rs1) Load Byte (Unsigned Extended) I rd = M[rs1+imm] [0:7]
lhu rd, imm(rs1) Load Half (Unsigned Extended) I rd = M[rs1+imm] [0:15]
Instruction Name FMT Description ©
sb rs2, imm(rs1) Store Byte S M[rs1+imm] [0:7] = rs2[0:7]
sh rs2, imm(rs1) Store Half S M[rs1+imm] [0:15] = rs2[0:15]
sw rs2, imm(rs1) Store Word S M[rs1+imm] [0:31] = rs2[0:31]

M 表示 Memory

pseudoinstruction Base Instruction(s) Meaning
li rd, immediate Myriad sequences Load immediate
mv rd, rs addi rd, rs, 0 Copy register
la rd, symbol auipc rd, delta[31:12] + delta[11]
addi rd, rd, delta[11:0]
Load absolute address,
where delta = symbol − pc

条件跳转指令 (branch)

Instruction Name FMT Description ©
beq rs1, rs2, imm Branch == B if (rs1 == rs2) PC += imm
bne rs1, rs2, imm Branch != B if (rs1 != rs2) PC += imm
blt rs1, rs2, imm Branch < B if (rs1 < rs2) PC += imm
bge rs1, rs2, imm Branch >= B if (rs1 >= rs2) PC += imm
bltu rs1, rs2, imm Branch < (U) B if (rs1 < rs2) PC += imm
bgeu rs1, rs2, imm Branch >= (U) B if (rs1 >= rs2) PC += imm

U 表示 Usigned Extended

pseudoinstruction Base Instruction(s) Meaning Description ©
ble rs1, rs2, offset bge rs2, rs1, offset Branch if Less or Equal if (rs1 <= rs2) PC += offset
bleu rs1, rs2, offset bgeu rs2, rs1, offset Branch if Less or Equal (U) if (rs1 <= rs2) PC += offset
bgt rs1, rs2, offset blt rs2, rs1, offset Branch if Greater Than if (rs > 0) PC += offset
bgtu rs1, rs2, offset bltu rs2, rs1, offset Branch if Greater Than (U) if (rs > 0) PC += offset
beqz rs, offset beq rs, x0, offset Branch if EQual Zero if (rs == 0) PC += offset
bnez rs, offset bne rs, x0, offset Branch if Not Equal Zero if (rs != 0) PC += offset
bltz rs, offset blt rs, x0, offset Branch if Less Than Zero if (rs < 0) PC += offset
blez rs, offset bge x0, rs, offset Branch if Less or Equal Zero if (rs <= 0) PC += offset
bgtz rs, offset blt x0, rs, offset Branch if Greater Than Zero if (rs > 0) PC += offset
bgez rs, offset bge rs, x0, offset Branch if Greater or Equal Zero if (rs >= 0) PC += offset

无条件跳转指令

Instruction Name FMT Description ©
jal rd, label Jump And Link J rd = PC+4; PC += imm
jalr rd, imm(rs1) Jump And Link Reg I rd = PC+4; PC = rs1 + imm

jal rd, label 跳转到 label 并将 jal 下一指令的地址存放到 rd 中
jalr rd, imm(rs1) 跳转到 rs1 + imm 并将 jalr 下一指令的地址存放到 rd 中

pseudoinstruction Base Instruction(s) Meaning
ret jalr x0, 0(x1) Return from subroutine
call offset auipc x1, offset[31:12] + offset[11]
jalr x1, offset[11:0](x1)
Call far-away subroutine

jalr x0, 0(x1) 表示将 ret 下一条指令的地址存放到 x0 中 (对 x0 的写操作不起作用), 并跳转到 x1 + 0 的位置 (即 x1 的位置), 其中 x1 是函数调用约定中用于存放函数返回地址的寄存器 (ra), 即 call 下一条指令的地址.

pseudoinstruction Base Instruction(s) Meaning
j offset jal x0, offset Jump
jr rs jalr x0, 0(rs) Jump register

常用伪操作 directive

.text: 代码段, 之后跟的符号都在 .text 内
.data: 数据段, 之后跟的符号都在 .data 内
.bss: 未初始化数据段, 之后跟的符号都在 .bss 中
.section .foo: 自定义段, 之后跟的符号都在 .foo 段中, .foo段名可以做修改

.align n: 按 2 的 n 次幂字节对齐
.balign n: 按 n 字节对齐

.global / .globl: 用于定义全局符号, 使链接过程中能被其他程序文件可见, 类似于 extern
.local: 用于定义局部符号, 仅当前程序文件可见

.string "str": 将字符串 str 放入内存
.byte b1,…,bn: 在内存中连续存储 n 个 bytes
.half w1,…,wn: 在内存中连续存储 n 个 half word (2 bytes)
.word w1,…,wn: 在内存中连续存储 n 个 word (4 bytes)
.dword w1,…,wn: 在内存中连续存储 n 个 double word (8字节)
.float f1,…,fn: 在内存中连续存储 n 个单精度浮点数
.double d1,…,dn: 在内存中连续存储 n 个双精度浮点数

函数调用约定

Register ABI Name Description Saver
x0 zero Hard-wired zero
x1 ra Return address Caller
x2 sp Stack pointer Callee
x3 gp Global pointer
x4 tp Thread pointer
x5 t0 Temporary/alternate link register Caller
x6–7 t1–2 Temporaries Caller
x8 s0/fp Saved register/frame pointer Callee
x9 s1 Saved register Callee
x10–11 a0–1 Function arguments/return values Caller
x12–17 a2–7 Function arguments Caller
x18–27 s2–11 Saved registers Callee
x28–31 t3–6 Temporaries Caller

ABI (Application Binary Interface)

零寄存器 (zero): 读取总为 0, 写入不进行任何操作.

返回地址寄存器 (ra): 用于存放函数返回的地址, 即 call 下一条指令的地址

栈指针寄存器 (sp): 用于存放栈指针

临时寄存器 (t0-t6): Callee 可能会使用这些寄存器, 所以 Callee 不保证这些寄存器中的值在函数调用过程中保持不变. 这意味着对于 Caller 来说, 如果调用 Callee 之后还要用到这些寄存器中的值, 则需要在调用 Callee 之前保存这些临时寄存器, 以及在 Callee 返回后重新恢复 t0-t6 的值, 以防止 t0-t6 的值在 Callee 中被修改.

保存寄存器 (s0-s11): Callee 需要保证这些寄存器的值在函数返回时维持函数调用之前的值. 所以一旦 Callee 在自己的函数中需要用到 s0-s11, 那么必须在使用之前将其值备份存储在栈中, 并在返回之前恢复 s0-s11 的值.

参数寄存器 (a0-a1): 用于在函数调用过程中保存第一个和第二个参数, 以及在函数返回时传递返回值;

参数寄存器 (a2-a7): 如果函数调用时需要传递更多的参数, 可以使用 a2-a7 这些寄存器. 但注意用于传递参数的寄存器最多只有 8 个, 如果还需要传递更多的参数, 则要借用栈来进行.

pseudoinstruction Base Instruction(s) Meaning
jal offset jal x1, offset Jump
jalr rs jalr x1, 0(rs) Jump register
j offset jal x0, offset Jump
jr rs jalr x0, 0(rs) Jump register
ret jalr x0, 0(x1) Return from subroutine
call offset auipc x1, offset[31:12] + offset[11]
jalr x1, offset[11:0](x1)
Call far-away subroutine
tail offset auipc x6, offset[31:12] + offset[11]
jalr x0, offset[11:0](x6)
Tail call far-away subroutine

尾调用 | wiki

使用栈 (stack)

1
2
3
4
5
6
7
8
void _start() {
// calling leaf routine
square(3);
}

int square(int num) {
return num * num;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    .text             # Define beginning of text section
.global _start # Define entry _start

_start:
la sp, stack_end # prepare stack for calling functions

li a0, 3
call square
# the time return here, a0 should stores the result

stop: j stop # dead loop to stop execution

# int square(int num)
square:
# prologue
addi sp, sp, -8
sw s0, 0(sp)
sw s1, 4(sp)

mul a0, a0, a0

# epilogue
lw s0, 0(sp)
lw s1, 4(sp)
addi sp, sp, 8
ret

# allocate stack space
stack_start:
.rept 12
.word 0
.endr
stack_end:

.end # End of file

注意在这个过程中函数栈以及栈指针是如何声明和使用的. 在整个程序的末尾声明了 stack_start 和 stack_end 两个 label, 方便程序对栈顶 stack_end (空栈顶) 和栈底 stack_start (满栈顶) 的地址进行引用

1
2
3
4
5
stack_start:  # low address
.rept 12 # repeat
.word 0 # store a 32-bit 0 in memory
.endr # end repeat
stack_end: # high address

并在 stack_start 和 stack_end 中通过 .rept/.endr 伪操作重复声明了 12 次 .word 0, 每个 .word 0 会存储一个 32-bit 的 0 在一段连续的 word (32bits) 空间上. 即声明了一段 48 bytes 的连续空间, 并初始化为 0.

在初始化栈指针寄存器 sp 时, 通过命令

1
la sp, stack_end

将 stack_end 的地址存放到 sp 寄存器中, 此时 sp 指向栈底 (空栈栈顶). 在压栈时, sp 的值从高地址向低地址变化, 并始终指向栈顶元素的起始地址.

1
2
3
addi sp, sp, -8
sw s0, 0(sp)
sw s1, 4(sp)
1
2
3
start_start ------------------------------------------ stack_end
-48 -47 ... -8 -7 -6 -5 -4 -3 -2 -1 (offset relative to stack_end)
|0x00|0x00|...|0x00|0x00|0x00|0x00|0x00|0x00|0x00|0x00|

汇编与 C 的混合编程

汇编调用 C 语言函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# test.s
.text # Define beginning of text section
.global _start # Define entry _start
.global foo # foo is a C function defined in test.c

_start:
la sp, stack_end # prepare stack for calling functions

# RISC-V uses a0 ~ a7 to transfer parameters
li a0, 1 # pass 1st argument
li a1, 2 # pass 2nd argument
call foo # <--
# RISC-V uses a0 & a1 to transfer return value
# check value of a0

stop: j stop # Infinite loop to stop execution

stack_start:
.rept 12
.word 0
.endr
stack_end:

.end # End of file
1
2
3
4
5
// test.c
int foo(int a, int b) {
int c = a + b;
return c;
}

C 语言调用汇编指令

1
2
3
4
5
6
asm [volatile] (
"汇编指令"
: 输出操作数列表 (可选)
: 输入操作数列表 (可选)
: 可能影响的寄存器或存储器 (可选)
);
1
2
3
4
5
6
7
8
9
int foo (int a, int b) {
int c;
asm volatile (
"add %[sum], %[add1], %[add2]"
:[sum]"=r"(c) // 输出操作数列表
:[add1]"r"(a), [add2]"r"(b) // 输入操作数列表
);
return c;
}
1
2
3
4
5
6
7
8
9
int foo (int a, int b) {
int c;
asm volatile (
"add %0, %1, %2"
:"=r"(c) // 输出操作数列表
:"r"(a), "r"(b) // 输入操作数列表
);
return c;
}
1
2
3
static void w_mscratch(reg_t x) {
asm volatile("csrw mscratch, %0" : : "r" (x));
}

C 语言调用汇编函数

1
2
extern void switch_to(struct context *next);
// switch_to defined in a assembly file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# void switch_to(struct context *next);
.globl switch_to
switch_to:
csrrw t6, mscratch, t6
beqz t6, 1f
reg_save t6
mv t5, t6
csrr t6, mscratch
STORE t6, 30*SIZE_REG(t5)
1:
csrw mscratch, a0
mv t6, a0
reg_restore t6
ret

CSR寄存器

RISCV 除了 32 个通用寄存器组之外, 每个权限级别 (Level): User / Supervisor / Machine 都还有一组额外的寄存器 – 控制与状态寄存器 (CSRs, Control and Status Registers).

高 Level 可以访问低 Level 的 CSRs, 反之不可以.

普通的 ISA 指令, 如 lw/sw 指令, 不能对 CSRs 寄存器进行操作. ISA Specification (“Zicsr” 扩展) 定义了特殊的 (专门的) CSR 指令来访问这些 CSRs 寄存器.

CSRRW (CSR Read and Write)
CSRRS (CSR Read and Set bits)
CSRRC (CSR Read and Clear bits)

CSRRWI (CSR Read and Write Immediate)
CSRRSI (CSR Read and Set bits Immediate)
CSRRCI (CSR Read and Clear bits Immediate)

Instruction Name Description ©
csrrw rd, csr, rs1 CSR Read and Write (Atomic) rd = csr; csr = rs1
csrrs rd, csr, rs1 CSR Read and Set bits (Atomic) rd = csr; csr |= rs1

csrrw t6, mscratch, t6 # swap t6 and mscratch

pseudoinstruction Base Instruction(s) Meaning
csrw csr, rs csrrw x0, csr, rs Write CSR
csrr rd, csr csrrs rd, csr, x0 Read CSR

Machine-level CSRs

Machine Information Registers:

Privilege Name Description
MRO mvendorid Vendor ID.
MRO marchid Architecture ID.
MRO mimpid Implementation ID.
MRO mhartid Hardware thread ID.

Machine Trap Setup:

Privilege Name Description
MRW mstatus Machine status register.
MRW misa ISA and extensions
MRW medeleg Machine exception delegation register.
MRW mideleg Machine interrupt delegation register.
MRW mie Machine interrupt-enable register.
MRW mtvec Machine trap-handler base address.
MRW mcounteren Machine counter enable.

Machine Trap Handling:

Privilege Name Description
MRW mscratch Scratch register for machine trap handlers.
MRW mepc Machine exception program counter.
MRW mcause Machine trap cause.
MRW mtval Machine bad address or instruction.
MRW mip Machine interrupt pending.

Machine Memory Protection:

Privilege Name Description
MRW pmpcfg0 Physical memory protection configuration.
MRW pmpcfg1 Physical memory protection configuration, RV32 only.
MRW pmpcfg2 Physical memory protection configuration.
MRW pmpcfg3 Physical memory protection configuration, RV32 only.
MRW pmpaddr0 Physical memory protection address register.
MRW pmpaddr1 Physical memory protection address register.
MRW pmpaddr15 Physical memory protection address register.

Trap 相关的 CSRs

mepc & mret

在 RISC-V 架构中, mepcmret 是与异常处理和中断返回相关的两个重要 CSR寄存器 和 CSR 指令, 主要用于 Machine 模式.

mepc 是 “Machine Exception Program Counter” 的缩写, 它是一个控制和状态寄存器(CSR), 用于保存发生异常或中断时的程序计数器 (PC) 值. 也就是说, 当一个异常或中断发生时, RISC-V 处理器会自动将当前的 PC 值保存到 mepc 寄存器中 (这里的 “自动” 是硬件上的实现), 以便异常处理程序处理完异常后知道从哪里恢复执行.

  • 当发生异常或中断时, 当前执行的指令地址 (PC) 会被保存到 mepc 中;
  • 在异常处理程序中, 处理器通过读取和修改 mepc 来决定异常处理结束后应该跳转到哪里;
  • 当使用 mret 指令时, 处理器会将 mepc 中的值恢复到 PC 中, 从而返回到异常或中断发生的那一条指令继续执行.

mret 是 “Machine Mode Return” 的缩写, 用于从 Machine 模式返回到之前的模式 (例如 User 模式或 Supervisor 模式). 当发生异常或中断时, 处理器会跳转到一个预定义的异常处理程序. 在异常处理程序执行完毕后, 使用 mret 指令可以将处理器状态恢复到异常或中断发生之前的状态, 并跳转回被中断的程序的地址继续执行.

  • 程序正常执行, 当遇到异常或中断时, 当前的 PC 值被保存到 mepc 中, 处理器跳转到异常处理程序;
  • 异常处理程序执行完毕后, 调用 mret 指令;
  • mret 指令将 mepc 中的值恢复到 PC 中, 处理器返回到异常或中断发生时的地址, 继续执行被中断的指令.

Trap 相关的 CSR 寄存器

Asynchronous Trap - Interrupt 中断 (异步的 Trap)
Synchronous Trap - Exception 异常 (同步的 Trap)

Privilege Name Description
MRW mtvec Machine Trap-Vector base address.
MRW mepc Machine Exception Program Counter.
MRW mcause Machine trap Cause.
MRW mtval Machine Trap Value.
MRW mstatus Machine Status register.
MRW mscratch Scratch register for machine trap handlers.
MRW mie Machine Interrupt-Enable register.
MRW mip Machine Interrupt Pending register.
  • mtvec (Machine Trap-Vector Base-Address): 它保存发生异常时处理器需要跳转到的地址, 即 trap_vector 函数的基地址.
  • mepc (Machine Exception Program Counter): 当 trap 发生时, hart 会将发生 trap 所对应指令的地址值 (pc) 保存在 mepc 中.
  • mcause (Machine Cause): 当 trap 发生时, hart 会设置该寄存器通知我们 trap 发生的原因.
  • mtval (Machine Trap Value): 它保存了 exception 发生时的附加信息, 譬如访问地址出错时的地址信息, 或者执行非法指令时的指令本身. 对于其他异常, 它的值为 0.
  • mstatus (Machine Status): 用于跟踪和控制 hart 的当前操作状态 (特别地, 包括关闭和打开全局中断).
  • mscratch (Machine Scratch): Machine 模式下专用寄存器, 我们可以自己定义其用法, 譬如用该寄存器保存当前在 hart 上运行的 task 的上下文 (context) 的地址.
  • mie (Machine Interrupt Enable): 用于进一步控制 (打开和关闭) software interrupt/timer interrupt/external interrupt.
  • mip (Machine Interrupt Pending): 它列出目前已发生等待处理的中断.

mtvec (Machine Trap-Vector) 寄存器

Machine trap-vector base-address register (mtvec)

1
2
3
 MXLEN-1(31)            2 1           0
| BASE(MXLEN-1:2) (WARL) | MODE (WARL) |
\_____base_address_____/ \____mode___/

RV32 的 MXLEN = 32, 这里的 MXLEN - 1 = 31
WARL: Write Any Values, Reads Legal Values

BASE: Trap 入口函数 trap_vector 的基地址, 必须保证 4 字节对齐;

关于 4 字节对齐. 完整的 base_address 应该是 32 位的, 但考虑到 base_address 是关于 4 字节对齐的, 即 base_address 的值是 4 的倍数 base_address % 4 = 0, 所以 base_address 值的后两位一定是 00. 于是这里可以只用 30 位 mtvec(31:2) 就对 base_address 进行存储 (存储 base_addres 的高 30 位).

MODE: 进一步控制入口函数地址的配置方式. Trap 入口函数的形式有 2 种方式, 一种叫做 Direct 方式, 一种叫做 Vectored 方式

Value Name Description
0 Direct All exceptions set pc to BASE.
1 Vectored Asynchronous interrupts set pc to BASE + 4 × cause.
>= 2 Reserved
  • Direct (MODE = 00): 所有的 Exception 和 Interrupt 发生后 PC 都跳转到 Base 指定的地址处. 即 Trap 处理函数只有 1 个, 在 Trap 处理函数中再通过 switch-case / if-else 等方式依据 Trap 类型进行分别处理.

  • Vectored (MODE = 01): Exception 处理方式同 Direct; 但 Interrupt 的入口函数的地址以数组方式排列. BASE 中保存这个指针数组的基地址.

    1
    2
    3
    4
    5
    6
    7
    ————————.- (base_address)
    pointer0| ——> trap_handler0
    ————————|- (base_addr + 1*4)
    pointer1| ——> trap_handler1
    ————————|- (base_addr + 2*4)
    pointer2| ——> trap_handler2
    ————————.

mcause (Machine Cause)

当 Trap 发生时, Hart 会设置该寄存器通知我们 Trap 发生的原因, 最高位 Interrupt 位为 1 时标识了当前 Trap 类型为 Interrupt, 为 0 时则标识为 Exception.

1
2
3
 MXLEN-1(31)  MXLEN-2(30)         0
| Interrupt | Exception Code (WLRL) |
\___ 1 ___/ \_____ MXLEN - 1 _____/

剩余的 Exception Code 用于标识具体的 Interrupt 或者 Exception 的种类

Software Interrupt 软件中断:

Interrupt Exception Code Description
1 0 User software interrupt
1 1 Supervisor software interrupt
1 2 Reserved for future standard use
1 3 Machine software interrupt

Time Interrupt 定时器中断:

Interrupt Exception Code Description
1 4 User timer interrupt
1 5 Supervisor timer interrupt
1 6 Reserved for future standard use
1 7 Machine timer interrupt

External Interrupt 外部中断:

Interrupt Exception Code Description
1 8 User external interrupt
1 9 Supervisor external interrupt
1 10 Reserved for future standard use
1 11 Machine external interrupt

Reserved:

Interrupt Exception Code Description
1 12-15 Reserved for future standard use
1 >=16 Reserved for platform use

Exception 异常:

Interrupt Exception Code Description
0 0 Instruction address misaligned
0 1 Instruction access fault
0 2 Illegal instruction
0 3 Breakpoint
0 4 Load address misaligned
0 5 Load access fault
0 6 Store/AMO address misaligned
0 7 Store/AMO access fault
0 8 Environment call from U-mode
0 9 Environment call from S-mode
0 10 Reserved
0 11 Environment call from M-mode
0 12 Instruction page fault
0 13 Load page fault
0 14 Reserved for future standard use
0 15 Store/AMO page fault
0 16–23 Reserved for future standard use
0 24–31 Reserved for custom use
0 32–47 Reserved for future standard use
0 48–63 Reserved for custom use
0 >=64 Reserved for future standard use

mstatus (Machine Status) 寄存器

Machine-mode status register

1
2
3
...| 12     11   10 9    8  |  7      6      5      4   |  3     2      1     0  |
...| MPP[1: 0] | WPRI | SPP | MPIE | WPRI | SPIE | UPIE | MIE | WPRI | SIE | UIE |
... \_ Previous Privilege _/ \______ Previous IE ______/ \__ Interrupt Enable __/
  • MIE/SIE/UIE (M/S/U Interrupt Enable)
    分别用于打开 (= 1) 或关闭 (= 0) Machine/Supervisor/User 模式下的全局中断. 当 Trap 发生时, Hart 会自动将 MIE/SIE/UIE 设置为 0, 即关闭全局中断.

  • MPIE/SPIE/UPIE (M/S/U Previous Interrupt Enable)
    当 Trap 发生时, 分别用于保存 Machine/Supervisor/User 模式下 Trap 发生之前的 MIE/SIE/UIE 的值.

  • MPP/SPP (M/S Previous Privilege)
    当 Trap 发生时, 分别用于保存 Trap 发生之前的权限级别. MPP 有 2 bits, 00/01/11 分别表示 Trap 之前是 User/Supervisor/Machine 模式; SPP 有 1 位, 0/1 分别表示 Trap 之前是 User/Superior 模式; 注意没有 UPP.
    Trap 发生时只能从低权限模式向高权限模式切换. 对于 Machine 模式可以从 User/Supervisor/Machine 模式切换到 (Trap) 到 Machine 模式; 对于 Supervisor 模式可以从 User/Supervisor 模式 Trap 到 Supervisor 模式; 对于 User 模式只能从 User 模式切换. 所以 MPP 需要 2 bits 进行编码 (3 中情况), SPP 需要 1 bits 进行编码 (2 中情况), 而 UPP 不需要额外进行编码.

    mstatus 的 MPP 域与系统模式的切换有关, 并且 mstatus 上电后默认为 0. 所以上电后 MPP 默认为 00 , 即 mret 后默认进入 user 态. 如果将其设为 11, 则调用 mret 会返回到 machine 态.

mie (Machine Interrupt Enable)

mie 寄存器用于进一步控制 M/S/U 权限模式下 Software/Timer/External Interrupt 三种中断的开关. 注意区别 mstatus 寄存器的 MIE/SIE/UIE 域是三种模式中断的全局控制开关, 而 mie 是一个二级的更细分的控制开关.

1
2
3
 MXLEN-1  12  11   10   9    8    7    6    5    4    3    2    1    0
| WPRI |MEIE|WPRI|SEIE|UEIE|MTIE|WPRI|STIE|UTIE|MSIE|WPRI|SSIE|USIE|
\_________________/ \_________________/ \_________________/
  • MEIE/SEIE/UEIE (M/S/U External Interrupt Enable) 分别用于控制 M/S/U 模式下的外部中断
  • MTIE/STIE/UTIE (M/S/U Timer Interrupt Enable) 分别用于控制 M/S/U 模式下的定时器中断
  • MSIE/SSIE/USIE (M/S/U Timer Interrupt Enable) 分别用于控制 M/S/U 模式下的软件中断

mip (Machine Interrupt Pending)

mip 寄存器用于获取当前 M/S/U 模式下对应的 External/Timer/Software 中断是否发生并在等待的状态

1
2
3
 MXLEN-1  12  11   10   9    8    7    6    5    4    3    2    1    0
| WPRI |MEIP|WPRI|SEIP|UEIP|MTIP|WPRI|STIP|UTIP|MSIP|WPRI|SSIP|USIP|
\_________________/ \_________________/ \_________________/
  • MEIP/SEIP/UEIP (M/S/U External Interrupt Pending) 分别用于获取 M/S/U 模式下的外部中断的 Pending 状态
  • MTIE/STIE/UTIE (M/S/U Timer Interrupt Pending) 分别用于获取 M/S/U 模式下的定时器中断的 Pending 状态
  • MSIE/SSIE/USIE (M/S/U Timer Interrupt Pending) 分别用于获取 M/S/U 模式下的软件中断的 Pending 状态

Trap 发生流程

1. Trap 初始化:

设置入口函数, 将 trap_vector (trap 入口函数) 的地址赋给 mtvec 寄存器

2. Trap 的 Top Half:

Trap 发生, Hart 自动执行如下状态转换, 这个过程是硬件实现的, 不需要软件操作.

  • 把 mstatus 的 MIE 值复制到 MPIE 中, 清除 mstatus 中的 MIE 标志位 (置 0), 效果是关闭全局中断, 防止新触发的中断打断 Trap.
  • 设置 mepc; 同时 PC 被设置为 mtvec (其中保存着 trap_vector 的基地址), 即程序跳转到 trap 入口函数
    • 对于 Exception, mepc 指向导致异常的指令的地址
    • 对于 Interrupt, mepc 指向被中断的指令的下一条指令的地址
  • 根据 trap 的种类设置 mcause, 井根据需要为 mtval 设置附加信息.
  • 将 trap 发生之前的权限模式保存在 mstatus 的 MPP 域中, 再把 hart 权限模式更改为 M (也就是说无论在任何 Level 下触发 trap, hart 首先切换到 Machine 模式)

3. Trap 的 Bottom Half:

这部分为 trap 入口函数 trap_vector 所需要做的事情. 这部分是软件上的实现.

  • 保存 (save) 当前控制流的上下文 (context) 信息 (利用 mscratch 寄存器)
  • 调用 C 语言的 trap_handler, 其中包含着 switch-case 的逻辑, 根据 mcause 寄存器的 Exception Code 域, 分支处理不同类型的 trap
  • 从 trap_handler 函数返回 (mepc 的值有可能需要调整)
  • 恢复 (restore) 上下文的信息
  • 执行 MRET 指令返回到 trap 之前的状态

上下文 context 信息指的是 x1-x31 除去 x0 的 31 个寄存器的状态

4. Trap 返回:

在 Trap 结束进行返回时, 不同模式会调用各自的返回命令

  • mret (Machine)
  • sret (Supervisor)
  • uret (User)

例如在 Machine 模式下执行完 trap_vector 后会调用 mret 指令进行返回.

mret 会做 3 件事情

  • 恢复权限级别: 将当前 Hart 的权限级别设为 mstatus.MPP. 再重新将 mstatus.MPP 设为 U (如果 Hart 不支持 U 则设为 M)
  • 恢复中断状态: 将 mstatus.MIE 设为 mstatus.MPIE; 将 mstatus.MPIE 的值重置为 1;
  • PC 跳转: pc = mepc. pc 跳转到 Trap 发生时保存的地址. 对于异常, 则为触发异常的指令的基地址, 对于中断则为触发中断的指令的下一条指令的基地址.

中断的分类

中断一共有三种, 由 Hart (Core) 内部触发的中断又称为局部中断 (Local Interrupt), 由 Hart 外部触发的中断又称为全局中断 (Global Interrupt)

  • Local Interrupt: Timer Interrupt, Software Interrupt
  • Global Interrupt: External Interrupt

PLIC 编程接口

PLIC, Platform-Level Interrupt Controller, 用于处理 External Interrupt (global interrupt 全局中断). local interrupt 局部中断 software interrupt 和 timer interrupt 由 CLINT (Core Local Interrupt) 处理.

1
2
3
4
5
6
#define PLIC_BASE 0x0c000000L
#define UART0_IRQ 10 // IRQ: Interrupt Request
/* start.S
csrr t0, mhartid # read current m-hart-id to t0
mv tp, t0 # keep CPU's hartid in its tp for later usage.*/
int hart; asm volatile("mv %0, tp" : "=r" (hart));
  • Priority: PLIC_base + IRQ_id * 4
    设置某一路中断源的优先级. Qemu-virt 支持 7 个优先级. 0 表示对该中断源禁用中断, 1 最低, 7 最高. 如果两个中断源优先级相同, 则根据中断源的 ID 值进一步区分优先级, ID 值越小优先级越高.

    1
    2
    #define PLIC_PRIORITY(id) (PLIC_BASE + (id) * 4)
    *(uint32_t*)PLIC_PRIORITY(UART0_IRQ) = 1;
  • Pending: PLIC_base + 0x1000 + IRQ_id / 32 * 4
    用于指示某一路中断源是否发生. 每个 PLIC 包含 2 个 32 位的Pending 寄存器, 每一个 bit 对应一个中断源, 如果 bit 位为 1 表示该中断源上发生了中断 (进入Pending 状态), 有待 hart 处理; 否则 bit 位为 0 表示该中断源上当前无中断发生.
    Pending 寄存器的 Pending 状态可以通过 claim 方式清除.
    第一个 Pending 寄存器的第 0 位对应不存在的 0 号中断源, 其值永远为0.

  • Enable: PLIC_base + 0x2000 + Hart_id * 0x80 + IRQ_id / 32 * 4
    针对某个 Hart 开启或关闭某一路中断源. 每个 Hart 有 2 个 Enable 寄存器 (Enable1 和 Enable2) 用于针对该 Hart 启动或者关闭某路中断源. 每个中断源对应 Enable 寄存器的一个 bit, 其中 Enable1 负责控制 1~31 号中断源; Enable2 负责控制 32 ~53 号中断源. 将对应的bit位设置为1表示使能该中断源, 否则表示关闭该中断源.

    1
    2
    #define PLIC_MENABLE(hart, id) (PLIC_BASE + 0x2000 + (hart) * 0x80 + ((id) / 32) * 4)
    *(uint32_t*)PLIC_MENABLE(hart, UART0_IRQ)= (1 << (UART0_IRQ % 32));
  • Threshold: PLIC_base + 0x200000 + Hart_id * 0x1000
    针对某个 Hart 设置中断源优先级的阈值. 每个 Hart 有一个 Threshold 寄存器用于设置中断优先级的阈值. 所有小于或等于该阈值的中断源, 即使发生了也会被 PLIC 丢弃. 特别的, 当阈值设为 0 时允许所有中断源发生中断; 当阈值设为 7 时丢弃所有中断源上发生的中断.

    1
    2
    #define PLIC_MTHRESHOLD(hart) (PLIC_BASE + 0x200000 + (hart) * 0x1000)
    *(uint32_t*)PLIC_MTHRESHOLD(hart) = 0;
  • Claim/Complete: PLIC_base + 0x200004 + Hart_id * 0x1000
    Claim 和 Complete 是同一个寄存器, 每个 Hart 一个. 对该寄存器执行读操作称之为 Claim, 即获取当前发生的最高优先级的 IRQ (Interrupt Request) 的 ID. Claim 成功后会清除对应的 Pending 位. 对该寄存器执行写操作称之为 Complete, 所谓 Complete 指的是通知 PLIC 对该 IRQ 的处理已经结束.

    1
    2
    #define PLIC_MCLAIM(hart) (PLIC_BASE + 0x200004 + (hart) * 0x1000)
    int irq = *(uint32_t*)PLIC_MCLAIM(hart);
    1
    2
    #define PLIC_MCOMPLETE(hart) (PLIC_BASE + 0x200004 + (hart) * 0x1000)
    *(uint32_t*)PLIC_MCOMPLETE(hart) = irq;

中断的触发方式

以外部中断为例, 外部中断有两种触发方式: “电平触发” 和 “跳沿触发” (边沿触发)

  • 电平触发方式
    若外部中断定义为电平触发方式, 外部中断申请触发器的状态随着 CPU 在每个机器周期采样到的外部中断输入线的电平变化而变化, 这能提高 CPU 对外部中断请求的响应速度. 当外部中断源被设定为电平触发方式时, 在中断服务程序返回之前, 外部中断请求输入必须无效 (即变为高电平), 否则 CPU 返回主程序后会再次响应中断. 所以电平触发方式适合于外部中断以低电平输入而且中断服务程序能清除外部中断请求源 (即外部中断输入电平又变为高电平) 的情况。
  • 跳沿触发方式
    外部中断若定义为跳沿触发方式, 外部中断申请触发器能锁存外部中断输入线上的负跳变. 即便是 CPU 暂时不能响应, 中断申请标志也不会丢失. 在这种方式里, 如果相继连续两次采样, 一个机器周期采样到外部中断输入为高, 下一个机器周期采样为低, 则置 “1” 中断申请触发器, 直到CPU响应此中断时才清 “0”. 这样不会丢失中断, 但输入的负脉冲宽度至少保持 12 个时钟周期 (若晶振频率为 6MHz, 则为 21xs), 才能被 CPU 采样到. 外部中断的跳沿触发方式适合于以负脉冲形式输入的外部中断请求.

CLINT 编程接口

CLINT, Core Local Interruptor, 涉及到 Timer Interrupt

mtime 寄存器

内存地址: CLINT_base + 0xbff8

系统全局唯一, 在 RV32 和 RV64 上都是 64-bit. mtime 寄存器相当于一个计数器, 从系统一上电开始就一直自增 (从 0 开始自增), 硬件保证该寄存器的值始终按照一个固定的频率递增. 并且上电复位时, 硬件负责将 mtime 的值恢复为 0. QEMU 中定时器的工作频率约为 10MHz, 即每 1 秒钟, mtime 寄存器自增 10 000 000. 所以在程序中, 我们可以用 mtime 自增 10 000 000 来表示 1 秒钟的时间间隔.

1
#define CLINT_MTIME (CLINT_BASE + 0xBFF8)

mtimecmp 寄存器

内存地址: CLINT_base + 0x400 + Hart_id * 8

1
#define CLINT_MTIMECMP(hartid) (CLINT_BASE + 0x4000 + 8 * (hartid))

每个 Hart 一个 mtimecmp 寄存器, 64-bit. 上电复位时, 系统不负责设置 mtimecmp 的初值.

当 mtime >= mtimecmp 时, CLINT 会产生一个 timer 中断. 如果要 enable (使能) 定时器中断

  1. 需要先 ebable (使能) 全局中断, 将 mstatus 寄存器的 MIE 域 (第 3 bit 位) 置 1, 即 mstatus |= 0x00 00 00 08
  2. 并且将 mie 的 MTIE 域 (第 7 bit 位) 置 1, 即 mie |= 0x00 00 00 80

当 timer 中断发生时, Hart 会设置 mip 的 MTIP 域, 表示 time interrupt pending. 可以通过向 mtimecmp 中写入新的值, 清除 (置 0) mip.MTIP.

系统模式的切换

User Mode --ecall–> Machine Mode
User Mode <–eret – Machine Mode

ecall, environment call and breakpoint

系统调用底层通过 ecall 指令实现 user mode 到 machine mode 的切换, ecall 本身是通过触发中断的方式让系统进入到 trap_handler

1
2
3
4
5
6
7
              |^^^^^^^^^^^^^^^\
func() {... ecall ... ret} exception --> trap_handler()
\____________________|

|^^^^^^^^^^^^^^^^^^\
trap_handler() {... sys_func() ... mret} sys_func() {... ret}
\_____________________________|